diff --git a/backend/server.js b/backend/server.js index 8953b65..543bb54 100644 --- a/backend/server.js +++ b/backend/server.js @@ -25,6 +25,8 @@ db.exec(` http_method TEXT NOT NULL DEFAULT 'GET', request_body TEXT, headers TEXT, + cache_mode TEXT NOT NULL DEFAULT 'normal', + gzip INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT 'pending', created_at INTEGER NOT NULL, finished_at INTEGER, @@ -33,21 +35,43 @@ db.exec(` ) `); +// Non-destructive migration for existing DBs +try { db.exec(`ALTER TABLE tests ADD COLUMN cache_mode TEXT NOT NULL DEFAULT 'normal'`); } catch {} +try { db.exec(`ALTER TABLE tests ADD COLUMN gzip INTEGER NOT NULL DEFAULT 1`); } catch {} + // Stream active test outputs (testId -> {process, clients}) const activeTests = {}; // Generate a k6 script from test params function generateScript(params) { - const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers } = params; + const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip } = params; - const parsedHeaders = (() => { + // Build the merged headers object + const userHeaders = (() => { try { return JSON.parse(headers || '{}'); } catch { return {}; } })(); - const headersJs = Object.entries(parsedHeaders) + // Cache control headers + if (cacheMode === 'no-cache' || cacheMode === 'bust') { + userHeaders['Cache-Control'] = 'no-cache, no-store'; + userHeaders['Pragma'] = 'no-cache'; + } + + // Encoding header + if (gzip === false || gzip === 0) { + userHeaders['Accept-Encoding'] = 'identity'; + } + // When gzip=true, omit the header entirely so k6 sends its default (gzip, deflate, br) + + const headersJs = Object.entries(userHeaders) .map(([k, v]) => ` '${k.replace(/'/g, "\\'")}': '${v.replace(/'/g, "\\'")}',`) .join('\n'); + // For cache-bust mode, append a random query param per iteration + const targetUrl = cacheMode === 'bust' + ? `\`${url}${url.includes('?') ? '&' : '?'}_cb=\${Date.now()}-\${Math.random().toString(36).slice(2)}\`` + : `'${url}'`; + const bodyLine = (httpMethod !== 'GET' && httpMethod !== 'HEAD' && requestBody) ? `const body = \`${requestBody.replace(/`/g, '\\`')}\`;` : 'const body = null;'; @@ -84,7 +108,7 @@ ${headersJs} timeout: '30s', }; - const res = http.${httpMethod.toLowerCase()}('${url}'${httpMethod !== 'GET' && httpMethod !== 'HEAD' ? ', body' : ''}, params); + const res = http.${httpMethod.toLowerCase()}(${targetUrl}${httpMethod !== 'GET' && httpMethod !== 'HEAD' ? ', body' : ''}, params); const ok = check(res, { 'status is 2xx': (r) => r.status >= 200 && r.status < 300, @@ -110,6 +134,8 @@ app.post('/api/tests', (req, res) => { httpMethod = 'GET', requestBody = '', headers = '{}', + cacheMode = 'normal', + gzip = true, } = req.body; if (!url || !url.startsWith('http')) { @@ -120,11 +146,11 @@ app.post('/api/tests', (req, res) => { const createdAt = Date.now(); db.prepare(` - INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, status, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?) - `).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, createdAt); + INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, cache_mode, gzip, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?) + `).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, cacheMode, gzip ? 1 : 0, createdAt); - const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers }); + const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip }); const tmpScript = path.join(os.tmpdir(), `k6-script-${id}.js`); fs.writeFileSync(tmpScript, script); @@ -208,7 +234,7 @@ app.get('/api/tests/:id/stream', (req, res) => { // GET /api/tests — list all tests app.get('/api/tests', (req, res) => { - const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all(); + const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, cache_mode, gzip, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all(); res.json(rows.map(r => ({ ...r, summary: r.summary ? JSON.parse(r.summary) : null }))); }); diff --git a/frontend/app.js b/frontend/app.js index 602e900..36e5593 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -30,6 +30,15 @@ methodSel.addEventListener('change', () => { bodyGroup.style.display = (m !== 'GET' && m !== 'HEAD') ? '' : 'none'; }); +// gzip toggle hint +const gzipChk = document.getElementById('gzip'); +const gzipHint = document.getElementById('gzip-hint'); +gzipChk.addEventListener('change', () => { + gzipHint.textContent = gzipChk.checked + ? 'k6 default — server may compress response' + : 'Sends Accept-Encoding: identity — forces uncompressed response'; +}); + // ---- Run Test ---- const form = document.getElementById('test-form'); const startBtn = document.getElementById('start-btn'); @@ -49,6 +58,8 @@ form.addEventListener('submit', async (e) => { const httpMethod = document.getElementById('httpMethod').value; const requestBody = document.getElementById('requestBody').value.trim(); const headers = document.getElementById('headers').value.trim() || '{}'; + const cacheMode = document.getElementById('cacheMode').value; + const gzip = document.getElementById('gzip').checked; // Validate headers JSON try { JSON.parse(headers); } catch { @@ -69,7 +80,7 @@ form.addEventListener('submit', async (e) => { res = await fetch('/api/tests', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers }), + body: JSON.stringify({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip }), }); } catch (err) { alert('Failed to start test: ' + err.message); @@ -227,12 +238,22 @@ async function loadHistory() { summaryLine = parts.join(' · '); } + const cacheBadge = t.cache_mode === 'bust' + ? 'cache-bust' + : t.cache_mode === 'no-cache' + ? 'no-cache' + : 'cached'; + const gzipBadge = t.gzip + ? 'gzip' + : 'no-gzip'; + item.innerHTML = `
${t.status} ${escHtml(t.url)} ${t.http_method} · ${t.vus} VUs · ${dur} + ${cacheBadge}${gzipBadge}
${date}
${summaryLine ? `
${escHtml(summaryLine)}
` : ''} diff --git a/frontend/index.html b/frontend/index.html index f302b1e..09f3bc1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -58,6 +58,27 @@
+
+ + +
+ +
+ +
+ +
+
k6 default — server may compress response
+
+