Skip to content

Commit d8ed25c

Browse files
committed
Show list of failed checks
1 parent b1121e2 commit d8ed25c

File tree

5 files changed

+184
-25
lines changed

5 files changed

+184
-25
lines changed

src/githubHelper.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods'
77
import {
88
cleanScriptPath,
99
extractTestRunId,
10+
fetchChecks,
1011
fetchTestRunSummary,
1112
} from './k6helper'
1213
import {
13-
generateMetricsSummary,
14+
generateMarkdownSummary,
1415
getTestRunStatusMarkdown,
1516
} from './markdownRenderer'
1617
import { TestRunUrlsMap } from './types'
@@ -176,9 +177,11 @@ export async function generatePRComment(
176177
core.debug('Generating PR comment')
177178

178179
const resultSummaryStrings = ['# Performance Test Results 🚀\n\n']
179-
let testRunIndex = 1
180+
let testRunIndex = 0
180181

181182
for (const [scriptPath, testRunUrl] of Object.entries(testRunUrlsMap)) {
183+
testRunIndex++
184+
182185
resultSummaryStrings.push(
183186
`## ${testRunIndex}. 🔗 [${cleanScriptPath(scriptPath)}](${testRunUrl})\n`
184187
)
@@ -200,16 +203,18 @@ export async function generatePRComment(
200203
}
201204

202205
resultSummaryStrings.push(
203-
getTestRunStatusMarkdown(testRunSummary.test_run_status)
206+
getTestRunStatusMarkdown(testRunSummary.run_status)
204207
)
205208

206-
const markdownSummary = generateMetricsSummary(
207-
testRunSummary.metrics_summary
209+
const checks = await fetchChecks(testRunId)
210+
211+
const markdownSummary = generateMarkdownSummary(
212+
testRunSummary.metrics_summary,
213+
checks
208214
)
209215

210216
resultSummaryStrings.push(markdownSummary)
211217
resultSummaryStrings.push('\n')
212-
testRunIndex++
213218
}
214219

215220
const comment = resultSummaryStrings.join('\n')

src/k6helper.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { ChildProcess, spawn } from 'child_process'
44
import path from 'path'
55
import { apiRequest } from './apiUtils'
66
import { parseK6Output } from './k6OutputParser'
7-
import { TestRunSummary, TestRunUrlsMap } from './types'
7+
import { Check, ChecksResponse, TestRunSummary, TestRunUrlsMap } from './types'
8+
9+
function getK6CloudBaseUrl(): string {
10+
return process.env.K6_CLOUD_BASE_URL || 'https://api.k6.io'
11+
}
812

913
/**
1014
* Validates the test paths by running `k6 inspect --execution-requirements` on each test file.
@@ -211,8 +215,31 @@ export function extractTestRunId(testRunUrl: string): string | null {
211215
export async function fetchTestRunSummary(
212216
testRunId: string
213217
): Promise<TestRunSummary | undefined> {
214-
const baseUrl = process.env.K6_CLOUD_BASE_URL || 'https://api.k6.io/cloud/v5'
215-
const url = `${baseUrl}/test_runs(${testRunId})/result_summary?$select=metrics_summary`
218+
const baseUrl = getK6CloudBaseUrl()
219+
const url = `${baseUrl}/cloud/v5/test_runs(${testRunId})/result_summary?$select=metrics_summary`
216220

217221
return apiRequest<TestRunSummary>(url)
218222
}
223+
224+
/**
225+
* Fetches the checks for a test run from the Grafana Cloud K6 API.
226+
* Uses retry mechanism with exponential backoff for reliability.
227+
* Will automatically retry on transient errors and server errors, but not on client errors like 404 or 401.
228+
*
229+
* @param {string} testRunId - The ID of the test run to fetch checks for
230+
* @returns {Promise<Check[]>} Array of checks or empty array if there was an error
231+
*/
232+
export async function fetchChecks(testRunId: string): Promise<Check[]> {
233+
const baseUrl = getK6CloudBaseUrl()
234+
const url = `${baseUrl}/loadtests/v4/test_runs(${testRunId})/checks?$select=name,metric_summary&$filter=group_id eq null`
235+
236+
const response = await apiRequest<ChecksResponse>(url)
237+
238+
// If the API request fails, return an empty array
239+
if (!response) {
240+
return []
241+
}
242+
243+
// Return the checks array from the response
244+
return response.value
245+
}

src/markdownRenderer.ts

+38-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { isNumber } from '@latest-version/orval-core'
12
import {
23
BrowserMetricSummary,
4+
Check,
35
ChecksMetricSummary,
46
GrpcMetricSummary,
57
HttpMetricSummary,
@@ -239,7 +241,8 @@ export function getBrowserMetricsMarkdown(
239241
* @returns Markdown string for checks metrics
240242
*/
241243
export function getChecksMarkdown(
242-
checksMetrics: ChecksMetricSummary | null
244+
checksMetrics: ChecksMetricSummary | null,
245+
checks: Check[] | null
243246
): string[] {
244247
if (
245248
!checksMetrics ||
@@ -253,15 +256,42 @@ export function getChecksMarkdown(
253256
const markdownSections = []
254257

255258
if (
256-
checksMetrics.successes != null &&
259+
isNumber(checksMetrics.successes) &&
257260
checksMetrics.successes < checksMetrics.total
258261
) {
259262
markdownSections.push(
260263
`- ❌ **${formatNumber(checksMetrics.total - checksMetrics.successes)}** out of **${formatNumber(checksMetrics.total)}** checks were not successful.`
261264
)
265+
266+
// Add checks that failed
267+
if (checks && checks.length > 0) {
268+
// Aggregate checks by name
269+
const checksByName: Record<
270+
string,
271+
{ success_count: number; fail_count: number }
272+
> = {}
273+
274+
// Group checks by name and aggregate metrics
275+
checks.forEach((check) => {
276+
const { name, metric_summary } = check
277+
if (!checksByName[name]) {
278+
checksByName[name] = { success_count: 0, fail_count: 0 }
279+
}
280+
checksByName[name].success_count += metric_summary.success_count
281+
checksByName[name].fail_count += metric_summary.fail_count
282+
})
283+
// List failed checks (those with fail_count > 0)
284+
Object.entries(checksByName)
285+
.filter(([, metrics]) => metrics.fail_count > 0)
286+
.forEach(([name, metrics]) => {
287+
markdownSections.push(
288+
` - \`${name}\`: Failed **${formatNumber(metrics.fail_count)}**, out of **${formatNumber(metrics.success_count + metrics.fail_count)}** times.`
289+
)
290+
})
291+
}
262292
} else {
263293
markdownSections.push(
264-
`-✅ All **${formatNumber(checksMetrics.total)}** checks were successful.`
294+
`- ✅ All **${formatNumber(checksMetrics.total)}** checks were successful.`
265295
)
266296
}
267297

@@ -331,16 +361,17 @@ export function getTestRunStatusMarkdown(
331361
* @param metricsSummary The complete metrics summary object
332362
* @returns Markdown string for all metrics
333363
*/
334-
export function generateMetricsSummary(
335-
metricsSummary: MetricsSummary | null | undefined
364+
export function generateMarkdownSummary(
365+
metricsSummary: MetricsSummary | null | undefined,
366+
checks: Check[] | null
336367
): string {
337368
if (!metricsSummary) return 'No metrics data available.'
338369

339370
const markdownSections: string[] = []
340371

341372
// Add checks summary
342373
markdownSections.push(
343-
...getChecksMarkdown(metricsSummary.checks_metric_summary)
374+
...getChecksMarkdown(metricsSummary.checks_metric_summary, checks)
344375
)
345376

346377
// Add thresholds summary
@@ -368,7 +399,5 @@ export function generateMetricsSummary(
368399
...getBrowserMetricsMarkdown(metricsSummary.browser_metric_summary)
369400
)
370401

371-
return markdownSections.length
372-
? markdownSections.join('\n')
373-
: 'No metrics data available.'
402+
return markdownSections.length ? markdownSections.join('\n') : ''
374403
}

src/types.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,29 @@ export type MetricsSummary = {
8282

8383
export type TestRunSummary = {
8484
metrics_summary: MetricsSummary
85-
test_run_status: number
85+
run_status: number
86+
}
87+
88+
/**
89+
* Interface for the metric summary of a check
90+
*/
91+
export interface CheckMetricSummary {
92+
fail_count: number
93+
success_count: number
94+
success_rate: number
95+
}
96+
97+
/**
98+
* Interface for a single check item
99+
*/
100+
export interface Check {
101+
metric_summary: CheckMetricSummary
102+
name: string
103+
}
104+
105+
/**
106+
* Interface for the checks API response
107+
*/
108+
export interface ChecksResponse {
109+
value: Check[]
86110
}

test/k6helper.test.ts

+80-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { spawn } from 'child_process'
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { apiRequest } from '../src/apiUtils'
34
import {
45
cleanScriptPath,
56
executeRunK6Command,
67
extractTestRunId,
8+
fetchChecks,
79
generateK6RunCommand,
810
isCloudIntegrationEnabled,
911
validateTestPaths,
1012
} from '../src/k6helper'
11-
import { generateMetricsSummary } from '../src/markdownRenderer'
13+
import { generateMarkdownSummary } from '../src/markdownRenderer'
1214
import { TestRunUrlsMap } from '../src/types'
1315

1416
// Mock child_process.spawn
@@ -37,6 +39,11 @@ vi.mock('@actions/core', () => ({
3739
setFailed: vi.fn(),
3840
}))
3941

42+
// Mock apiRequest to return specific responses for different tests
43+
vi.mock('../src/apiUtils', () => ({
44+
apiRequest: vi.fn(),
45+
}))
46+
4047
describe('cleanScriptPath', () => {
4148
const originalEnv = process.env
4249

@@ -241,13 +248,17 @@ describe('extractTestRunId', () => {
241248
})
242249
})
243250

244-
describe('generateMetricsSummary', () => {
251+
describe('generateMarkdownSummary', () => {
245252
it('should return a default message for null metrics', () => {
246-
expect(generateMetricsSummary(null)).toBe('No metrics data available.')
253+
expect(generateMarkdownSummary(null, null)).toBe(
254+
'No metrics data available.'
255+
)
247256
})
248257

249258
it('should return a default message for undefined metrics', () => {
250-
expect(generateMetricsSummary(undefined)).toBe('No metrics data available.')
259+
expect(generateMarkdownSummary(undefined, null)).toBe(
260+
'No metrics data available.'
261+
)
251262
})
252263

253264
it('should return a default message for empty metrics', () => {
@@ -260,7 +271,7 @@ describe('generateMetricsSummary', () => {
260271
browser_metric_summary: null,
261272
}
262273

263-
expect(generateMetricsSummary(emptyMetrics)).toBe(
274+
expect(generateMarkdownSummary(emptyMetrics, null)).toBe(
264275
'No metrics data available.'
265276
)
266277
})
@@ -278,8 +289,71 @@ describe('generateMetricsSummary', () => {
278289
browser_metric_summary: null,
279290
}
280291

281-
const result = generateMetricsSummary(metrics)
292+
const result = generateMarkdownSummary(metrics, null)
282293

283294
expect(result).toContain('checks were not successful')
284295
})
285296
})
297+
298+
describe('fetchChecks', () => {
299+
beforeEach(() => {
300+
vi.resetAllMocks()
301+
})
302+
303+
it('should return checks array when API request succeeds', async () => {
304+
// Mock response from the API
305+
const mockChecksResponse = {
306+
'@count': 3,
307+
value: [
308+
{
309+
group_id: null,
310+
id: 'c43e6629-af3b-59c4-a047-531dca123851',
311+
metric_summary: {
312+
fail_count: 10,
313+
success_count: 0,
314+
success_rate: 0.0,
315+
},
316+
name: 'black friday is present',
317+
scenario_id: 'efeb799c-a053-56bf-9a26-c59d78537bc0',
318+
},
319+
{
320+
group_id: null,
321+
id: 'f9c6591a-3195-525f-a1e8-c39173a65056',
322+
metric_summary: {
323+
fail_count: 0,
324+
success_count: 10,
325+
success_rate: 1.0,
326+
},
327+
name: 'Connected successfully',
328+
scenario_id: 'efeb799c-a053-56bf-9a26-c59d78537bc0',
329+
},
330+
],
331+
}
332+
333+
// Mock the apiRequest function to return our mock response
334+
vi.mocked(apiRequest).mockResolvedValueOnce(mockChecksResponse)
335+
336+
// Call the function
337+
const result = await fetchChecks('1234')
338+
339+
// Verify the result
340+
expect(result).toEqual(mockChecksResponse.value)
341+
expect(apiRequest).toHaveBeenCalledWith(
342+
expect.stringContaining('/test_runs(1234)/checks')
343+
)
344+
})
345+
346+
it('should return empty array when API request fails', async () => {
347+
// Mock the apiRequest function to return undefined (API failure)
348+
vi.mocked(apiRequest).mockResolvedValueOnce(undefined)
349+
350+
// Call the function
351+
const result = await fetchChecks('1234')
352+
353+
// Verify the result
354+
expect(result).toEqual([])
355+
expect(apiRequest).toHaveBeenCalledWith(
356+
expect.stringContaining('/test_runs(1234)/checks')
357+
)
358+
})
359+
})

0 commit comments

Comments
 (0)