Skip to content

Commit efe2516

Browse files
committed
feat(vitest): write run summary when using GitHub Actions Reporter
1 parent 4d390df commit efe2516

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

packages/vitest/src/node/reporters/github-actions.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { Vitest } from '../core'
44
import type { TestProject } from '../project'
55
import type { Reporter } from '../types/reporter'
66
import type { TestCase, TestModule } from './reported-tasks'
7+
import { writeFileSync } from 'node:fs'
8+
import { relative } from 'node:path'
79
import { stripVTControlCharacters } from 'node:util'
810
import { getFullName, getTasks } from '@vitest/runner/utils'
911
import { capturePrintError } from '../printError'
@@ -109,6 +111,19 @@ export class GithubActionsReporter implements Reporter {
109111
})
110112
this.ctx.logger.log(`\n${formatted}`)
111113
}
114+
115+
if (process.env.GITHUB_STEP_SUMMARY) {
116+
try {
117+
writeFileSync(
118+
process.env.GITHUB_STEP_SUMMARY,
119+
renderSummary(collectSummaryData(testModules)),
120+
{ flag: 'a' },
121+
)
122+
}
123+
catch (error) {
124+
this.ctx.logger.warn('Could not write summary to $GITHUB_STEP_SUMMARY', error)
125+
}
126+
}
112127
}
113128
}
114129

@@ -165,3 +180,111 @@ function escapeProperty(s: string): string {
165180
.replace(/:/g, '%3A')
166181
.replace(/,/g, '%2C')
167182
}
183+
184+
interface SummaryData {
185+
flakyTests: Array<{
186+
path: {
187+
relative: string
188+
absolute: string
189+
}
190+
tests: Array<{
191+
testName: string
192+
line: number | undefined
193+
retries: {
194+
allowed: number
195+
count: number
196+
ratio: number
197+
}
198+
}>
199+
}>
200+
}
201+
202+
function collectSummaryData(testModules: ReadonlyArray<TestModule>): SummaryData {
203+
const summaryData: SummaryData = { flakyTests: [] }
204+
205+
for (const module of testModules) {
206+
const flakyTests: SummaryData['flakyTests'][number] = {
207+
path: { relative: module.relativeModuleId, absolute: module.moduleId },
208+
tests: [],
209+
}
210+
211+
for (const test of module.children.allTests()) {
212+
const diagnostic = test.diagnostic()
213+
214+
if (diagnostic?.flaky) {
215+
const retriesAllowed = typeof test.options.retry === 'number'
216+
? test.options.retry
217+
: (test.options.retry?.count
218+
// falling back to `retryCount` as this is used as the denominator to compute `retryRatio`
219+
?? diagnostic.retryCount)
220+
const retriesRatio = diagnostic.retryCount / retriesAllowed
221+
222+
flakyTests.tests.push({
223+
retries: {
224+
allowed: retriesAllowed,
225+
count: diagnostic.retryCount,
226+
ratio: retriesRatio,
227+
},
228+
line: test.task.location?.line,
229+
testName: test.task.fullTestName,
230+
})
231+
}
232+
}
233+
234+
if (flakyTests.tests.length > 0) {
235+
flakyTests.tests.sort((a, b) => b.retries.ratio - a.retries.ratio)
236+
237+
summaryData.flakyTests.push(flakyTests)
238+
}
239+
}
240+
241+
return summaryData
242+
}
243+
244+
function createGitHubFileLinkCreator(): (path: string, line?: number) => string | null {
245+
const repository = process.env.GITHUB_REPOSITORY
246+
const commitHash = process.env.GITHUB_SHA
247+
const rootPath = process.env.GITHUB_WORKSPACE
248+
249+
if (repository !== undefined && commitHash !== undefined && rootPath !== undefined) {
250+
return (path, line) => {
251+
const lineFragment = line !== undefined ? `#L${line}` : ''
252+
253+
return `https://github.com/${repository}/blob/${commitHash}/${relative(rootPath, path)}${lineFragment}`
254+
}
255+
}
256+
257+
return () => null
258+
}
259+
260+
function mdLink(text: string, url: string | null): string {
261+
return url === null ? text : `[${text}](${url})`
262+
}
263+
264+
function renderSummary(summaryData: SummaryData): string {
265+
const fileLinkCreator = createGitHubFileLinkCreator()
266+
267+
let summary = '## Vitest Test Report\n'
268+
269+
if (summaryData.flakyTests.length > 0) {
270+
summary += '\n### Flaky Tests\n\nThese tests passed only after one or more retries, indicating potential instability.\n'
271+
272+
for (const flakyTests of summaryData.flakyTests) {
273+
summary += `\n##### \`${flakyTests.path.relative}\` (${flakyTests.tests.length} flaky tests)\n`
274+
275+
for (const flakyTest of flakyTests.tests) {
276+
const retriesText = `passed on retry ${flakyTest.retries.count} out of ${flakyTest.retries.allowed}`
277+
278+
summary += `\n- ${mdLink(`\`${flakyTest.testName}\``, fileLinkCreator(flakyTests.path.absolute, flakyTest.line))} (${flakyTest.retries.ratio >= 0.8 ? `**${retriesText}**` : retriesText})`
279+
}
280+
281+
summary += '\n'
282+
}
283+
}
284+
285+
if (!summary.endsWith('\n')) {
286+
summary += '\n'
287+
}
288+
289+
return summary
290+
}

0 commit comments

Comments
 (0)