Skip to content

Commit 1ec78f0

Browse files
badeballdavidjgoss
andauthored
Deterministic usage formatter (#2633)
Co-authored-by: David Goss <[email protected]>
1 parent 1279ef7 commit 1ec78f0

File tree

14 files changed

+214
-58
lines changed

14 files changed

+214
-58
lines changed

exports/root/report.api.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export class Formatter {
130130
protected stream: Writable;
131131
// (undocumented)
132132
protected supportCodeLibrary: SupportCodeLibrary;
133+
// (undocumented)
134+
protected usageOrder: UsageOrder;
133135
}
134136

135137
// @public (undocumented)
@@ -155,6 +157,7 @@ declare namespace formatterHelpers {
155157
formatLocation,
156158
formatSummary,
157159
getUsage,
160+
UsageOrder,
158161
GherkinDocumentParser,
159162
PickleParser
160163
}
@@ -189,7 +192,7 @@ function getStepKeyword({ pickleStep, gherkinStepMap, }: IGetStepKeywordRequest)
189192
function getStepKeywordType({ keyword, language, previousKeywordType, }: IGetStepKeywordTypeOptions): KeywordType;
190193

191194
// @public (undocumented)
192-
function getUsage({ stepDefinitions, eventDataCollector, }: IGetUsageRequest): IUsage[];
195+
function getUsage({ stepDefinitions, eventDataCollector, order, }: IGetUsageRequest): IUsage[];
193196

194197
declare namespace GherkinDocumentParser {
195198
export {
@@ -516,6 +519,14 @@ export class UsageJsonFormatter extends Formatter {
516519
replacer(key: string, value: any): any;
517520
}
518521

522+
// @public (undocumented)
523+
enum UsageOrder {
524+
// (undocumented)
525+
EXECUTION_TIME = "EXECUTION_TIME",
526+
// (undocumented)
527+
LOCATION = "LOCATION"
528+
}
529+
519530
// @public (undocumented)
520531
export const version: string;
521532

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"John McLaughlin <[email protected]>",
8080
"John Wright <[email protected]>",
8181
"Johny Jose <[email protected]>",
82+
"Jonas Amundsen (https://github.com/badeball)",
8283
"Jonathan Gomez <[email protected]>",
8384
"Jonathan Kim <[email protected]>",
8485
"Josh Chisholm <[email protected]>",

src/formatter/helpers/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export { KeywordType, getStepKeywordType } from './keyword_type'
77
export { formatIssue, isWarning, isFailure, isIssue } from './issue_helpers'
88
export { formatLocation } from './location_helpers'
99
export { formatSummary } from './summary_helpers'
10-
export { getUsage } from './usage_helpers'
10+
export { getUsage, UsageOrder } from './usage_helpers'
1111
export { GherkinDocumentParser, PickleParser }

src/formatter/helpers/usage_helpers/index.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ export interface IUsage {
2222
uri: string
2323
}
2424

25+
export enum UsageOrder {
26+
EXECUTION_TIME = 'EXECUTION_TIME',
27+
LOCATION = 'LOCATION',
28+
}
29+
2530
export interface IGetUsageRequest {
2631
eventDataCollector: EventDataCollector
2732
stepDefinitions: StepDefinition[]
33+
order?: UsageOrder
2834
}
2935

3036
function buildEmptyMapping(
@@ -53,7 +59,7 @@ const unexecutedStatuses: readonly messages.TestStepResultStatus[] = [
5359
function buildMapping({
5460
stepDefinitions,
5561
eventDataCollector,
56-
}: IGetUsageRequest): Record<string, IUsage> {
62+
}: Omit<IGetUsageRequest, 'order'>): Record<string, IUsage> {
5763
const mapping = buildEmptyMapping(stepDefinitions)
5864
eventDataCollector.getTestCaseAttempts().forEach((testCaseAttempt) => {
5965
const pickleStepMap = getPickleStepMap(testCaseAttempt.pickle)
@@ -91,15 +97,22 @@ function normalizeDuration(duration?: messages.Duration): number {
9197
return messages.TimeConversion.durationToMilliseconds(duration)
9298
}
9399

94-
function buildResult(mapping: Record<string, IUsage>): IUsage[] {
100+
function buildResult(
101+
mapping: Record<string, IUsage>,
102+
order: UsageOrder
103+
): IUsage[] {
95104
return Object.keys(mapping)
96105
.map((stepDefinitionId) => {
97106
const { matches, ...rest } = mapping[stepDefinitionId]
98107
const sortedMatches = matches.sort((a: IUsageMatch, b: IUsageMatch) => {
99108
if (a.duration === b.duration) {
100109
return a.text < b.text ? -1 : 1
101110
}
102-
return normalizeDuration(b.duration) - normalizeDuration(a.duration)
111+
if (order === UsageOrder.EXECUTION_TIME) {
112+
return normalizeDuration(b.duration) - normalizeDuration(a.duration)
113+
} else {
114+
return a.text.localeCompare(b.text)
115+
}
103116
})
104117
const result = { matches: sortedMatches, ...rest }
105118
const durations: messages.Duration[] = matches
@@ -116,16 +129,22 @@ function buildResult(mapping: Record<string, IUsage>): IUsage[] {
116129
}
117130
return result
118131
})
119-
.sort(
120-
(a: IUsage, b: IUsage) =>
121-
normalizeDuration(b.meanDuration) - normalizeDuration(a.meanDuration)
122-
)
132+
.sort((a: IUsage, b: IUsage) => {
133+
if (order === UsageOrder.EXECUTION_TIME) {
134+
return (
135+
normalizeDuration(b.meanDuration) - normalizeDuration(a.meanDuration)
136+
)
137+
} else {
138+
return a.uri.localeCompare(b.uri)
139+
}
140+
})
123141
}
124142

125143
export function getUsage({
126144
stepDefinitions,
127145
eventDataCollector,
146+
order = UsageOrder.EXECUTION_TIME,
128147
}: IGetUsageRequest): IUsage[] {
129148
const mapping = buildMapping({ stepDefinitions, eventDataCollector })
130-
return buildResult(mapping)
149+
return buildResult(mapping, order)
131150
}

src/formatter/helpers/usage_helpers/index_spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it } from 'mocha'
22
import { expect } from 'chai'
33
import { getEnvelopesAndEventDataCollector } from '../../../../test/formatter_helpers'
44
import { buildSupportCodeLibrary } from '../../../../test/runtime_helpers'
5-
import { getUsage } from './'
5+
import { getUsage, UsageOrder } from './'
66

77
describe('Usage Helpers', () => {
88
describe('getUsage', () => {
@@ -23,6 +23,7 @@ describe('Usage Helpers', () => {
2323
const output = getUsage({
2424
eventDataCollector,
2525
stepDefinitions: supportCodeLibrary.stepDefinitions,
26+
order: UsageOrder.EXECUTION_TIME,
2627
})
2728

2829
// Assert
@@ -55,6 +56,7 @@ describe('Usage Helpers', () => {
5556
const output = getUsage({
5657
eventDataCollector,
5758
stepDefinitions: supportCodeLibrary.stepDefinitions,
59+
order: UsageOrder.EXECUTION_TIME,
5860
})
5961

6062
// Assert

src/formatter/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SupportCodeLibrary } from '../support_code_library_builder/types'
44
import { valueOrDefault } from '../value_checker'
55
import { FormatterPlugin } from '../plugin'
66
import { IColorFns } from './get_color_fns'
7-
import { EventDataCollector } from './helpers'
7+
import { EventDataCollector, UsageOrder } from './helpers'
88
import StepDefinitionSnippetBuilder from './step_definition_snippet_builder'
99
import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax'
1010

@@ -17,6 +17,9 @@ export interface FormatOptions {
1717
html?: {
1818
externalAttachments?: boolean
1919
}
20+
usage?: {
21+
order?: UsageOrder
22+
}
2023
rerun?: FormatRerunOptions
2124
snippetInterface?: SnippetInterface
2225
snippetSyntax?: string
@@ -51,6 +54,7 @@ export default class Formatter {
5154
protected stream: Writable
5255
protected supportCodeLibrary: SupportCodeLibrary
5356
protected printAttachments: boolean
57+
protected usageOrder: UsageOrder
5458
private readonly cleanup: IFormatterCleanupFn
5559
static readonly documentation: string
5660

@@ -67,6 +71,10 @@ export default class Formatter {
6771
options.parsedArgvOptions.printAttachments,
6872
true
6973
)
74+
this.usageOrder = valueOrDefault(
75+
options.parsedArgvOptions.usage?.order,
76+
UsageOrder.EXECUTION_TIME
77+
)
7078
}
7179

7280
async finished(): Promise<void> {

src/formatter/usage_formatter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default class UsageFormatter extends Formatter {
2222
const usage = getUsage({
2323
stepDefinitions: this.supportCodeLibrary.stepDefinitions,
2424
eventDataCollector: this.eventDataCollector,
25+
order: this.usageOrder,
2526
})
2627
if (usage.length === 0) {
2728
this.log('No step definitions')

src/formatter/usage_formatter_spec.ts

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { expect } from 'chai'
33
import FakeTimers, { InstalledClock } from '@sinonjs/fake-timers'
44
import { reindent } from 'reindent-template-literals'
55
import timeMethods from '../time'
6-
import { getUsageSupportCodeLibrary } from '../../test/fixtures/usage_steps'
6+
import {
7+
getBasicUsageSupportCodeLibrary,
8+
getOrderedUsageSupportCodeLibrary,
9+
} from '../../test/fixtures/usage/usage_steps'
710
import { testFormatter } from '../../test/formatter_helpers'
11+
import { UsageOrder } from './helpers'
812

913
describe('UsageFormatter', () => {
1014
let clock: InstalledClock
@@ -33,7 +37,7 @@ describe('UsageFormatter', () => {
3337
describe('unused', () => {
3438
it('outputs the step definitions as unused', async () => {
3539
// Arrange
36-
const supportCodeLibrary = getUsageSupportCodeLibrary(clock)
40+
const supportCodeLibrary = getBasicUsageSupportCodeLibrary(clock)
3741

3842
// Act
3943
const output = await testFormatter({
@@ -47,11 +51,11 @@ describe('UsageFormatter', () => {
4751
┌────────────────┬──────────┬───────────────────┐
4852
│ Pattern / Text │ Duration │ Location │
4953
├────────────────┼──────────┼───────────────────┤
50-
│ abc │ UNUSED │ usage_steps.ts:11
54+
│ abc │ UNUSED │ usage_steps.ts:13
5155
├────────────────┼──────────┼───────────────────┤
52-
│ /def?/ │ UNUSED │ usage_steps.ts:16
56+
│ /def?/ │ UNUSED │ usage_steps.ts:18
5357
├────────────────┼──────────┼───────────────────┤
54-
│ ghi │ UNUSED │ usage_steps.ts:25
58+
│ ghi │ UNUSED │ usage_steps.ts:27
5559
└────────────────┴──────────┴───────────────────┘
5660
5761
`)
@@ -70,7 +74,7 @@ describe('UsageFormatter', () => {
7074
uri: 'a.feature',
7175
},
7276
]
73-
const supportCodeLibrary = getUsageSupportCodeLibrary(clock)
77+
const supportCodeLibrary = getBasicUsageSupportCodeLibrary(clock)
7478

7579
// Act
7680
const output = await testFormatter({
@@ -86,13 +90,13 @@ describe('UsageFormatter', () => {
8690
┌────────────────┬──────────┬───────────────────┐
8791
│ Pattern / Text │ Duration │ Location │
8892
├────────────────┼──────────┼───────────────────┤
89-
│ abc │ UNUSED │ usage_steps.ts:11
93+
│ abc │ UNUSED │ usage_steps.ts:13
9094
├────────────────┼──────────┼───────────────────┤
91-
│ /def?/ │ - │ usage_steps.ts:16
95+
│ /def?/ │ - │ usage_steps.ts:18
9296
│ de │ - │ a.feature:4 │
9397
│ def │ - │ a.feature:3 │
9498
├────────────────┼──────────┼───────────────────┤
95-
│ ghi │ UNUSED │ usage_steps.ts:25
99+
│ ghi │ UNUSED │ usage_steps.ts:27
96100
└────────────────┴──────────┴───────────────────┘
97101
98102
`)
@@ -101,15 +105,15 @@ describe('UsageFormatter', () => {
101105
})
102106

103107
describe('not in dry run', () => {
104-
it('outputs the step definition without durations', async () => {
108+
it('outputs the step definition with durations', async () => {
105109
// Arrange
106110
const sources = [
107111
{
108112
data: 'Feature: a\nScenario: b\nWhen def\nThen de',
109113
uri: 'a.feature',
110114
},
111115
]
112-
const supportCodeLibrary = getUsageSupportCodeLibrary(clock)
116+
const supportCodeLibrary = getBasicUsageSupportCodeLibrary(clock)
113117

114118
// Act
115119
const output = await testFormatter({
@@ -124,19 +128,91 @@ describe('UsageFormatter', () => {
124128
┌────────────────┬──────────┬───────────────────┐
125129
│ Pattern / Text │ Duration │ Location │
126130
├────────────────┼──────────┼───────────────────┤
127-
│ /def?/ │ 1.50ms │ usage_steps.ts:16
131+
│ /def?/ │ 1.50ms │ usage_steps.ts:18
128132
│ def │ 2.00ms │ a.feature:3 │
129133
│ de │ 1.00ms │ a.feature:4 │
130134
├────────────────┼──────────┼───────────────────┤
131-
│ abc │ UNUSED │ usage_steps.ts:11
135+
│ abc │ UNUSED │ usage_steps.ts:13
132136
├────────────────┼──────────┼───────────────────┤
133-
│ ghi │ UNUSED │ usage_steps.ts:25
137+
│ ghi │ UNUSED │ usage_steps.ts:27
134138
└────────────────┴──────────┴───────────────────┘
135139
136140
`)
137141
)
138142
})
139143
})
144+
145+
describe('sorting', () => {
146+
const sources = [
147+
{
148+
data: 'Feature: a\nScenario: a\nGiven foo\nThen bar',
149+
uri: 'a.feature',
150+
},
151+
{
152+
data: 'Feature: b\nScenario: b\nGiven foo\nThen bar',
153+
uri: 'b.feature',
154+
},
155+
]
156+
157+
it('defaults to order by execution time, decreasingly', async () => {
158+
const supportCodeLibrary = getOrderedUsageSupportCodeLibrary(clock)
159+
160+
// Act
161+
const output = await testFormatter({
162+
sources,
163+
supportCodeLibrary,
164+
type: 'usage',
165+
})
166+
167+
// Assert
168+
expect(output).to.eql(
169+
reindent(`
170+
┌────────────────┬──────────┬─────────────────┐
171+
│ Pattern / Text │ Duration │ Location │
172+
├────────────────┼──────────┼─────────────────┤
173+
│ foo │ 15.00ms │ foo_steps.ts:10 │
174+
│ foo │ 20.00ms │ b.feature:3 │
175+
│ foo │ 10.00ms │ a.feature:3 │
176+
├────────────────┼──────────┼─────────────────┤
177+
│ bar │ 3.00ms │ bar_steps.ts:10 │
178+
│ bar │ 4.00ms │ b.feature:4 │
179+
│ bar │ 2.00ms │ a.feature:4 │
180+
└────────────────┴──────────┴─────────────────┘
181+
182+
`)
183+
)
184+
})
185+
186+
it('can optionally order by location', async () => {
187+
const supportCodeLibrary = getOrderedUsageSupportCodeLibrary(clock)
188+
189+
// Act
190+
const output = await testFormatter({
191+
sources,
192+
supportCodeLibrary,
193+
type: 'usage',
194+
parsedArgvOptions: { usage: { order: UsageOrder.LOCATION } },
195+
})
196+
197+
// Assert
198+
expect(output).to.eql(
199+
reindent(`
200+
┌────────────────┬──────────┬─────────────────┐
201+
│ Pattern / Text │ Duration │ Location │
202+
├────────────────┼──────────┼─────────────────┤
203+
│ bar │ 3.00ms │ bar_steps.ts:10 │
204+
│ bar │ 2.00ms │ a.feature:4 │
205+
│ bar │ 4.00ms │ b.feature:4 │
206+
├────────────────┼──────────┼─────────────────┤
207+
│ foo │ 15.00ms │ foo_steps.ts:10 │
208+
│ foo │ 10.00ms │ a.feature:3 │
209+
│ foo │ 20.00ms │ b.feature:3 │
210+
└────────────────┴──────────┴─────────────────┘
211+
212+
`)
213+
)
214+
})
215+
})
140216
})
141217
})
142218
})

src/formatter/usage_json_formatter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default class UsageJsonFormatter extends Formatter {
2121
const usage = getUsage({
2222
stepDefinitions: this.supportCodeLibrary.stepDefinitions,
2323
eventDataCollector: this.eventDataCollector,
24+
order: this.usageOrder,
2425
})
2526
this.log(JSON.stringify(usage, this.replacer, 2))
2627
}

0 commit comments

Comments
 (0)