Skip to content

Commit

Permalink
feat: add test list view
Browse files Browse the repository at this point in the history
  • Loading branch information
Ma11hewThomas committed Nov 5, 2024
1 parent f983d78 commit d3b975f
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 100 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/detailed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
run: npx tsc
- name: Test detailed with title
run: node dist/index.js tests ctrf-reports/ctrf-report.json --title "Detailed With Title"
- name: Test default no title
run: node dist/index.js ctrf-reports/ctrf-report.json --annotate false
- name: Test list
run: node dist/index.js test-list ctrf-reports/ctrf-report.json --annotate false --title "List With Title"
- name: Upload test results
uses: actions/upload-artifact@v4
with:
Expand Down
34 changes: 34 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { generateSkippedTestsDetailsTable } from './views/skipped'
import { generateFailedFoldedTable } from './views/failed-folded'
import { generateTestSuiteFoldedTable } from './views/suite-folded'
import { generateSuiteListView } from './views/suite-list'
import { generateTestListView } from './views/test-list'

interface Arguments {
_: Array<string | number>
Expand Down Expand Up @@ -75,6 +76,16 @@ const argv: Arguments = yargs(hideBin(process.argv))
})
}
)
.command(
'test-list <file>',
'Generate test list from a CTRF report',
(yargs) => {
return yargs.positional('file', {
describe: 'Path to the CTRF file',
type: 'string',
})
}
)
.command(
'failed <file>',
'Generate fail test report from a CTRF report',
Expand Down Expand Up @@ -391,6 +402,29 @@ if ((commandUsed === 'all' || commandUsed === '') && argv.file) {
} catch (error) {
console.error('Failed to read file:', error)
}
} else if (argv._.includes('test-list') && argv.file) {
try {
let report = validateCtrfFile(argv.file)
report = stripAnsiFromErrors(report)
if (report !== null) {
if (argv.title) {
addHeading(title)
}
generateTestListView(report.results.tests, useSuiteName)
write()
if (argv.prComment) {
postPullRequestComment(report, apiUrl, baseUrl, onFailOnly, title, useSuiteName, prCommentMessage)
}
if (pullRequest) {
postPullRequestComment(report, apiUrl, baseUrl, onFailOnly, title, useSuiteName, core.summary.stringify())
}
if (exitOnFail) {
exitActionOnFail(report)
}
}
} catch (error) {
console.error('Failed to read file:', error)
}
} else if (argv._.includes('failed') && argv.file) {
try {
let report = validateCtrfFile(argv.file)
Expand Down
147 changes: 58 additions & 89 deletions src/views/flaky-rate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,111 +50,80 @@ export async function generateFlakyRateSummary(
}
>()

reports.forEach((run) => {
const { tests } = run.results

tests.forEach((test) => {
const testName = getTestName(test, useSuiteName)

let data = flakyTestMap.get(testName)
if (!data) {
data = {
testName,
attempts: 0,
pass: 0,
fail: 0,
flakes: 0,
flakeRate: 0,
const calculateFlakeRate = (reportSubset: CtrfReport[]) => {
reportSubset.forEach((run) => {
const { tests } = run.results

tests.forEach((test) => {
const testName = getTestName(test, useSuiteName)

let data = flakyTestMap.get(testName)
if (!data) {
data = {
testName,
attempts: 0,
pass: 0,
fail: 0,
flakes: 0,
flakeRate: 0,
}
flakyTestMap.set(testName, data)
}
flakyTestMap.set(testName, data)
}

if (test.status === 'passed' || test.status === 'failed') {
const testRuns = 1 + (test.retries || 0)
data.attempts += testRuns
if (test.status === 'passed' || test.status === 'failed') {
const testRuns = 1 + (test.retries || 0)
data.attempts += testRuns

let isFlaky = false
let isFlaky = false

if (test.flaky) {
isFlaky = true
} else if (test.retries && test.retries > 0 && test.status === 'passed') {
isFlaky = true
}
if (test.flaky) {
isFlaky = true
} else if (test.retries && test.retries > 0 && test.status === 'passed') {
isFlaky = true
}

if (isFlaky) {
data.flakes += test.retries || 0
}
if (isFlaky) {
data.flakes += test.retries || 0
}

if (test.status === 'passed') {
data.pass += 1
data.fail += test.retries || 0
} else if (test.status === 'failed') {
data.fail += 1 + (test.retries || 0)
if (test.status === 'passed') {
data.pass += 1
data.fail += test.retries || 0
} else if (test.status === 'failed') {
data.fail += 1 + (test.retries || 0)
}
}
}
})
})
})

const flakyTestArray = Array.from(flakyTestMap.values())

flakyTestArray.forEach((data) => {
data.flakeRate = data.attempts > 0 ? (data.flakes / data.attempts) * 100 : 0
})

const totalAttemptsAllTests = flakyTestArray.reduce(
(sum, data) => sum + data.attempts,
0
)
const totalFlakesAllTests = flakyTestArray.reduce(
(sum, data) => sum + data.flakes,
0
)
const overallFlakeRate =
totalAttemptsAllTests > 0 ? (totalFlakesAllTests / totalAttemptsAllTests) * 100 : 0
const overallFlakeRateFormatted = overallFlakeRate.toFixed(2)
const overallFlakeRateMessage = `**Overall Flaky Rate:** ${overallFlakeRateFormatted}%`

const flakyTestArrayNonZero = flakyTestArray.filter(
(data) => data.flakeRate > 0
)

const totalRuns = reports.length
const totalRunsMessage = `<sub><i>Measured over ${totalRuns} runs.</i></sub>`

if (flakyTestArrayNonZero.length === 0) {
const noFlakyMessage = `<sub><i>No flaky tests detected over ${totalRuns} runs.</i></sub>`
const summary = `
${overallFlakeRateMessage}
${noFlakyMessage}
[Github Test Reporter CTRF](https://github.com/ctrf-io/github-test-reporter)
`
core.summary.addRaw(summary)
return
return Array.from(flakyTestMap.values()).reduce(
(sum, data) => (data.attempts > 0 ? sum + data.flakes / data.attempts : sum),
0
) * 100
}

flakyTestArrayNonZero.sort((a, b) => b.flakeRate - a.flakeRate)

const flakyRows = flakyTestArrayNonZero.map((data) => {
const { testName, attempts, pass, fail, flakeRate } = data
return `| ${testName} | ${attempts} | ${pass} | ${fail} | ${flakeRate.toFixed(
2
)}% |`
})
// Normal flake rate using all test results
const overallFlakeRate = calculateFlakeRate(reports)

const limitedSummaryRows = flakyRows.slice(0, rows)
// Adjusted flake rate by excluding the latest 5 test results
const adjustedReports = reports.slice(5)
const adjustedFlakeRate = calculateFlakeRate(adjustedReports)

const summaryTable = `
${overallFlakeRateMessage}
// Calculate flake rate change
const flakeRateChange = overallFlakeRate - adjustedFlakeRate
const flakeRateChangeMessage = `**Flake Rate Change:** ${flakeRateChange.toFixed(
2
)}%`

| Test 📝| Attempts 🎯| Pass ✅| Fail ❌| Flaky Rate 🍂|
| --- | --- | --- | --- | --- |
${limitedSummaryRows.join('\n')}
const overallFlakeRateMessage = `**Overall Flaky Rate:** ${overallFlakeRate.toFixed(2)}%`
const adjustedFlakeRateMessage = `**Adjusted Flaky Rate:** ${adjustedFlakeRate.toFixed(2)}%`

${totalRunsMessage}
const summary = `
${overallFlakeRateMessage}
${adjustedFlakeRateMessage}
${flakeRateChangeMessage}
[Github Test Reporter CTRF](https://github.com/ctrf-io/github-test-reporter-ctrf)
`
core.summary.addRaw(summaryTable)
core.summary.addRaw(summary)
}
17 changes: 8 additions & 9 deletions src/views/suite-list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as core from '@actions/core'
import { CtrfTest } from '../../types/ctrf'
import { getEmojiForStatus } from './common'
import { stripAnsi } from '../common'

export function generateSuiteListView(tests: CtrfTest[], useSuite: boolean): void {
try {
Expand All @@ -11,8 +13,8 @@ export function generateSuiteListView(tests: CtrfTest[], useSuite: boolean): voi

tests.forEach((test) => {
const groupKey = useSuite
? test.suite || 'Unknown Suite'
: (test.filePath || 'Unknown File').replace(workspacePath, '').replace(/^\//, '')
? test.suite || 'No suite provided'
: (test.filePath || 'No file path provided').replace(workspacePath, '').replace(/^\//, '')

if (!testResultsByGroup[groupKey]) {
testResultsByGroup[groupKey] = { tests: [], statusEmoji: '✅' }
Expand All @@ -33,18 +35,15 @@ export function generateSuiteListView(tests: CtrfTest[], useSuite: boolean): voi
markdown += `## ${groupData.statusEmoji} ${escapeMarkdown(groupKey)}\n\n`

groupData.tests.forEach((test) => {
const statusEmoji =
test.status === 'passed' ? '✅' :
test.status === 'failed' ? '❌' :
test.status === 'skipped' ? '⏭️' :
test.status === 'pending' ? '⏳' : '❓'
const statusEmoji = getEmojiForStatus(test.status)

const testName = escapeMarkdown(test.name || 'Unnamed Test')
const testName = escapeMarkdown(test.name)

markdown += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;**${statusEmoji} ${testName}**\n`

if (test.status === 'failed' && test.message) {
const message = test.message.replace(/\n{2,}/g, '\n').trim()
let message = stripAnsi(test.message || "No failure message")
message = message.replace(/\n{2,}/g, '\n').trim()

const escapedMessage = escapeMarkdown(message)

Expand Down
48 changes: 48 additions & 0 deletions src/views/test-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as core from '@actions/core'
import { CtrfTest } from '../../types/ctrf'
import { getTestName, stripAnsi } from '../common'
import { getEmojiForStatus } from './common'

export function generateTestListView(tests: CtrfTest[], useSuiteName: boolean): void {
try {
let markdown = `\n`

function escapeMarkdown(text: string): string {
return text.replace(/([\\*_{}[\]()#+\-.!])/g, '\\$1')
}

tests.forEach((test) => {
const statusEmoji = getEmojiForStatus(test.status)

const testName = escapeMarkdown(getTestName(test, useSuiteName))

markdown += `**${statusEmoji} ${testName}**\n`

if (test.status === 'failed') {
let message = stripAnsi(test.message || "No failure message")
message = message.replace(/\n{2,}/g, '\n').trim()

const escapedMessage = escapeMarkdown(message)

const indentedMessage = escapedMessage
.split('\n')
.filter(line => line.trim() !== '')
.map(line => `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${line}`)
.join('\n')

markdown += `${indentedMessage}\n`
}
})

markdown += `\n[Github Test Reporter](https://github.com/ctrf-io/github-test-reporter)`

core.summary.addRaw(markdown)

} catch (error) {
if (error instanceof Error) {
core.setFailed(`Failed to display test list: ${error.message}`)
} else {
core.setFailed('An unknown error occurred')
}
}
}

0 comments on commit d3b975f

Please sign in to comment.