Skip to content

Commit d4a31b2

Browse files
committed
implement suite-level attributes
1 parent 830c285 commit d4a31b2

File tree

6 files changed

+236
-2
lines changed

6 files changed

+236
-2
lines changed

javascript/package-lock.json

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

javascript/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
"@cucumber/message-streams": "^4.0.1",
2929
"@cucumber/messages": "27.0.0",
3030
"@types/chai": "^4.3.11",
31+
"@types/chai-almost": "^1.0.3",
3132
"@types/chai-xml": "^0.3.6",
3233
"@types/mocha": "^10.0.6",
3334
"@types/node": "18.11.18",
3435
"@typescript-eslint/eslint-plugin": "5.48.1",
3536
"@typescript-eslint/parser": "5.48.1",
3637
"chai": "^4.3.10",
38+
"chai-almost": "^1.0.1",
3739
"chai-xml": "^0.4.1",
3840
"eslint": "8.32.0",
3941
"eslint-config-prettier": "8.6.0",

javascript/src/ExtendedQuery.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import {
3+
Envelope,
4+
getWorstTestStepResult,
5+
TestCase,
6+
TestCaseFinished,
7+
TestCaseStarted,
8+
TestRunFinished,
9+
TestRunStarted,
10+
TestStepResultStatus,
11+
TimeConversion,
12+
} from '@cucumber/messages'
13+
import { Query as CucumberQuery } from '@cucumber/query'
14+
15+
export class ExtendedQuery extends CucumberQuery {
16+
private testRunStarted: TestRunStarted
17+
private testRunFinished: TestRunFinished
18+
private readonly testCaseById: Map<string, TestCase> = new Map()
19+
private readonly testCaseStartedById: Map<string, TestCaseStarted> = new Map()
20+
private readonly testCaseFinishedByTestCaseStartedId: Map<string, TestCaseFinished> = new Map()
21+
private readonly finalAttemptByTestCaseId: Map<
22+
string,
23+
[TestCase, TestCaseStarted, TestCaseFinished]
24+
> = new Map()
25+
26+
update(envelope: Envelope) {
27+
super.update(envelope)
28+
29+
if (envelope.testRunStarted) {
30+
this.testRunStarted = envelope.testRunStarted
31+
}
32+
if (envelope.testCase) {
33+
this.testCaseById.set(envelope.testCase.id, envelope.testCase)
34+
}
35+
if (envelope.testCaseStarted) {
36+
this.testCaseStartedById.set(envelope.testCaseStarted.id, envelope.testCaseStarted)
37+
}
38+
if (envelope.testCaseFinished) {
39+
this.testCaseFinishedByTestCaseStartedId.set(
40+
envelope.testCaseFinished.testCaseStartedId,
41+
envelope.testCaseFinished
42+
)
43+
if (!envelope.testCaseFinished.willBeRetried) {
44+
const testCaseStarted = this.testCaseStartedById.get(
45+
envelope.testCaseFinished.testCaseStartedId
46+
)!
47+
const testCase = this.testCaseById.get(testCaseStarted.testCaseId)!
48+
this.finalAttemptByTestCaseId.set(testCase.id, [
49+
testCase,
50+
testCaseStarted,
51+
envelope.testCaseFinished,
52+
])
53+
}
54+
}
55+
if (envelope.testRunFinished) {
56+
this.testRunFinished = envelope.testRunFinished
57+
}
58+
}
59+
60+
findTestRunDuration() {
61+
if (!this.testRunStarted || !this.testRunFinished) {
62+
return undefined
63+
}
64+
return TimeConversion.millisecondsToDuration(
65+
TimeConversion.timestampToMillisecondsSinceEpoch(this.testRunFinished.timestamp) -
66+
TimeConversion.timestampToMillisecondsSinceEpoch(this.testRunStarted.timestamp)
67+
)
68+
}
69+
70+
countMostSevereTestStepResultStatus(): Record<TestStepResultStatus, number> {
71+
const result: Record<TestStepResultStatus, number> = {
72+
[TestStepResultStatus.AMBIGUOUS]: 0,
73+
[TestStepResultStatus.FAILED]: 0,
74+
[TestStepResultStatus.PASSED]: 0,
75+
[TestStepResultStatus.PENDING]: 0,
76+
[TestStepResultStatus.SKIPPED]: 0,
77+
[TestStepResultStatus.UNDEFINED]: 0,
78+
[TestStepResultStatus.UNKNOWN]: 0,
79+
}
80+
for (const [testCase] of this.finalAttemptByTestCaseId.values()) {
81+
const statusesFromSteps = testCase.testSteps.map((testStep) => {
82+
return getWorstTestStepResult(this.getTestStepResults(testStep.id))
83+
})
84+
const overallStatus = getWorstTestStepResult(statusesFromSteps)
85+
result[overallStatus.status]++
86+
}
87+
return result
88+
}
89+
}

javascript/src/helpers.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { TestStepResultStatus } from '@cucumber/messages'
2+
import { expect, use } from 'chai'
3+
import chaiAlmost from 'chai-almost'
4+
5+
import { countStatuses, durationToSeconds } from './helpers.js'
6+
7+
use(chaiAlmost())
8+
9+
describe('helpers', () => {
10+
describe('durationToSeconds', () => {
11+
it('returns zero when no duration present', () => {
12+
expect(durationToSeconds(undefined)).to.eq(0)
13+
})
14+
15+
it('returns duration in seconds', () => {
16+
expect(
17+
durationToSeconds({
18+
seconds: 3,
19+
nanos: 987654,
20+
})
21+
).to.almost.eq(3.000987654)
22+
})
23+
})
24+
25+
describe('countStatuses', () => {
26+
const statuses: Record<TestStepResultStatus, number> = {
27+
[TestStepResultStatus.AMBIGUOUS]: 1,
28+
[TestStepResultStatus.FAILED]: 2,
29+
[TestStepResultStatus.PASSED]: 3,
30+
[TestStepResultStatus.PENDING]: 4,
31+
[TestStepResultStatus.SKIPPED]: 5,
32+
[TestStepResultStatus.UNDEFINED]: 6,
33+
[TestStepResultStatus.UNKNOWN]: 7,
34+
}
35+
36+
it('counts for all statuses when no predicate supplied', () => {
37+
expect(countStatuses(statuses)).to.eq(28)
38+
})
39+
40+
it('honours a supplied predicate', () => {
41+
expect(countStatuses(statuses, (status) => status === TestStepResultStatus.UNDEFINED)).to.eq(
42+
6
43+
)
44+
})
45+
})
46+
})

javascript/src/helpers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Duration, TestStepResultStatus, TimeConversion } from '@cucumber/messages'
2+
3+
export function durationToSeconds(duration?: Duration) {
4+
if (!duration) {
5+
return 0
6+
}
7+
return TimeConversion.durationToMilliseconds(duration) / 1000
8+
}
9+
10+
export function countStatuses(
11+
statuses: Record<TestStepResultStatus, number>,
12+
predicate: (status: TestStepResultStatus) => boolean = () => true
13+
) {
14+
return Object.entries(statuses)
15+
.filter(([status]) => predicate(status as TestStepResultStatus))
16+
.reduce((prev, [, curr]) => prev + curr, 0)
17+
}

javascript/src/index.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Envelope } from '@cucumber/messages'
1+
import { Query as GherkinQuery } from '@cucumber/gherkin-utils'
2+
import { Envelope, TestStepResultStatus } from '@cucumber/messages'
3+
import xmlbuilder from 'xmlbuilder'
4+
5+
import { ExtendedQuery } from './ExtendedQuery.js'
6+
import { countStatuses, durationToSeconds } from './helpers.js'
27

38
export default {
49
type: 'formatter',
@@ -9,6 +14,35 @@ export default {
914
on: (type: 'message', handler: (message: Envelope) => void) => void
1015
write: (content: string) => void
1116
}) {
12-
on('message', (message) => {})
17+
const gherkinQuery = new GherkinQuery()
18+
const cucumberQuery = new ExtendedQuery()
19+
const builder = xmlbuilder
20+
.create('testsuite', { invalidCharReplacement: '' })
21+
.att('name', 'Cucumber')
22+
23+
on('message', (message) => {
24+
gherkinQuery.update(message)
25+
cucumberQuery.update(message)
26+
27+
if (message.testRunFinished) {
28+
builder.att('time', durationToSeconds(cucumberQuery.findTestRunDuration()))
29+
const statusCounts = cucumberQuery.countMostSevereTestStepResultStatus()
30+
builder.att('tests', countStatuses(statusCounts))
31+
builder.att(
32+
'skipped',
33+
countStatuses(statusCounts, (status) => status === TestStepResultStatus.SKIPPED)
34+
)
35+
builder.att(
36+
'failures',
37+
countStatuses(
38+
statusCounts,
39+
(status) =>
40+
status !== TestStepResultStatus.PASSED && status !== TestStepResultStatus.SKIPPED
41+
)
42+
)
43+
builder.att('errors', 0)
44+
write(builder.end({ pretty: true }))
45+
}
46+
})
1347
},
1448
}

0 commit comments

Comments
 (0)