Skip to content

Commit 6476597

Browse files
authored
Use a message based usage formatter (#3086)
Use a message based usage formatter. The --plugin usage now produces a human readable report where for each step definition usage statistics are calculated. Individual duration for each step are also included. ``` Expression/Text Duration Mean ± Error Location I have {int} cukes in my belly 0.002s 0.002s ± 0.000s io.cucumber.skeleton.StepDefinitions.I_have_cukes_in_my_belly(int) I have 42 cukes in my belly 0.002s src/test/resources/io/cucumber/skeleton/belly.feature:3 a forgotten step, unused, but not unloved io.cucumber.skeleton.StepDefinitions.an_unused_step() UNUSED ``` With this implementation any unused step definitions also automatically show up in the report as step definitions without steps. This also allow the report to be reused for the unused steps formatter. ``` 1 unused step definition(s) Location Expression io.cucumber.skeleton.StepDefinitions.an_unused_step() # a forgotten step, unused, but not unloved ``` A `--plugin usage-json` was added to provide a json based report. This reports structure differs significantly from the original json output from the usage plugin.
1 parent ac14488 commit 6476597

File tree

14 files changed

+442
-562
lines changed

14 files changed

+442
-562
lines changed

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
~~# Changelog
1+
# Changelog
22

33
All notable changes to the current version this project will be documented in
44
this file. For previous versions see the [release-notes archive](release-notes).
@@ -13,8 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
### Fixed
1414
- [Core] Prefer URIs with authority ([#3098](https://github.com/cucumber/cucumber-jvm/pull/3098) M.P. Korstanje)
1515

16+
### Added
17+
- [Core] Add a `UsageJsonFormatter`, use with `--plugin usage-json` ([#3086](https://github.com/cucumber/cucumber-jvm/pull/3086) M.P. Korstanje)
18+
1619
### Changed
1720
- [Core] Use a message based `TimeLineFormatter` ([#3095](https://github.com/cucumber/cucumber-jvm/pull/3095) M.P. Korstanje)
21+
- [Core] Use a message based `UsageFormatter` ([#3086](https://github.com/cucumber/cucumber-jvm/pull/3086) M.P. Korstanje)
22+
- [Core] Use a message based `UnusedFormatter` ([#3086](https://github.com/cucumber/cucumber-jvm/pull/3086) M.P. Korstanje)
23+
1824

1925
## [7.30.0] - 2025-10-01
2026
### Changed
@@ -579,4 +585,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
579585
[7.2.0]: https://github.com/cucumber/cucumber-jvm/compare/v7.1.0...v7.2.0
580586
[7.1.0]: https://github.com/cucumber/cucumber-jvm/compare/v7.0.0...v7.1.0
581587
[7.0.0]: https://github.com/cucumber/cucumber-jvm/compare/v7.0.0-RC1...v7.0.0
582-
[7.0.0-RC1]: https://github.com/cucumber/cucumber-jvm/compare/v6.11.0...v7.0.0-RC1~~
588+
[7.0.0-RC1]: https://github.com/cucumber/cucumber-jvm/compare/v6.11.0...v7.0.0-RC1

cucumber-bom/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<tag-expressions.version>8.0.0</tag-expressions.version>
2626
<teamcity-formatter.version>0.1.1</teamcity-formatter.version>
2727
<testng-xml-formatter.version>0.6.0</testng-xml-formatter.version>
28+
<usage-formatter.version>0.1.0</usage-formatter.version>
2829
</properties>
2930

3031
<dependencyManagement>
@@ -90,6 +91,11 @@
9091
<artifactId>testng-xml-formatter</artifactId>
9192
<version>${testng-xml-formatter.version}</version>
9293
</dependency>
94+
<dependency>
95+
<groupId>io.cucumber</groupId>
96+
<artifactId>usage-formatter</artifactId>
97+
<version>${usage-formatter.version}</version>
98+
</dependency>
9399

94100
<!-- Modules in cucumber-jvm -->
95101
<dependency>

cucumber-core/README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ cucumber.glue= # comma separated package names.
5151
5252
cucumber.plugin= # comma separated plugin strings.
5353
# example: pretty, json:path/to/report.json
54+
# example: com.example.MyCustomPlugin:path/to/report.xml
5455
5556
cucumber.object-factory= # object factory class name.
5657
# example: com.example.MyObjectFactory
@@ -104,9 +105,28 @@ The performance gain on real projects depends on the feature size.
104105

105106
When not specified, the `RandomUuidGenerator` is used.
106107

107-
## Plugin ##
108+
## Built in plugins ##
108109

109-
By implementing the Plugin interface, classes can listen to execution events
110+
Cucumber comes with several built-in plugins. See the configuration options or the CLIs `--help` command for guidance on
111+
activating them.
112+
113+
* [html](https://github.com/cucumber/html-formatter): Renders a html report of the text execution
114+
* [json](https://github.com/cucumber/cucumber-json-formatter): Renders a json report of the text execution. In maintenance mode.
115+
* [junit](https://github.com/cucumber/junit-xml-formatter): Renders a JUnit xml report of the text execution.
116+
* [progress](https://github.com/cucumber/pretty-formatter): Renders sequence of dots indicating test execution progress.
117+
* [message](https://github.com/cucumber/messages): Logs cucumbers execution as a stream of json messages.
118+
* rerun: Creates a file with scenarios that should be rerun.
119+
* [summary](https://github.com/cucumber/pretty-formatter): Renders summary report of the test execution.
120+
* [testng](https://github.com/cucumber/testng-xml-formatter): Renders a TestNG xml report of the text execution.
121+
* timeline: Renders a timeline of the test execution.
122+
* [unused](https://github.com/cucumber/usage-formatter): Renders a plain text report of unused step definitions.
123+
* [usage](https://github.com/cucumber/usage-formatter): Renders a plain text report of step definition usage statistics.
124+
* [usage-json](https://github.com/cucumber/usage-formatter): Renders a json text report of step definition usage statistics.
125+
* [teamcity](https://github.com/cucumber/teamcity-formatter/): Interspaces Cucumbers output with TeamCity Service Messages
126+
127+
## Custom Plugins ##
128+
129+
By implementing the `Plugin` interface, classes can listen to execution events
110130
inside Cucumber JVM. Consider using a Plugin when creating test execution reports.
111131

112132
## FileSystem ##

cucumber-core/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<properties>
1515
<project.Automatic-Module-Name>io.cucumber.core</project.Automatic-Module-Name>
1616
<apiguardian-api.version>1.1.2</apiguardian-api.version>
17+
<assertj.version>3.27.6</assertj.version>
1718
<jackson.version>2.20.0</jackson.version>
1819
<jsoup.version>1.21.2</jsoup.version>
1920
<junit-jupiter.version>5.14.0</junit-jupiter.version>
@@ -48,6 +49,13 @@
4849
<type>pom</type>
4950
<scope>import</scope>
5051
</dependency>
52+
<dependency>
53+
<groupId>org.assertj</groupId>
54+
<artifactId>assertj-bom</artifactId>
55+
<version>${assertj.version}</version>
56+
<type>pom</type>
57+
<scope>import</scope>
58+
</dependency>
5159
</dependencies>
5260
</dependencyManagement>
5361

@@ -76,6 +84,10 @@
7684
<groupId>io.cucumber</groupId>
7785
<artifactId>testng-xml-formatter</artifactId>
7886
</dependency>
87+
<dependency>
88+
<groupId>io.cucumber</groupId>
89+
<artifactId>usage-formatter</artifactId>
90+
</dependency>
7991
<dependency>
8092
<groupId>io.cucumber</groupId>
8193
<artifactId>tag-expressions</artifactId>
@@ -188,12 +200,25 @@
188200
<scope>test</scope>
189201
</dependency>
190202

203+
<dependency>
204+
<groupId>org.hamcrest</groupId>
205+
<artifactId>hamcrest</artifactId>
206+
<version>${hamcrest.version}</version>
207+
<scope>test</scope>
208+
</dependency>
209+
191210
<dependency>
192211
<groupId>org.skyscreamer</groupId>
193212
<artifactId>jsonassert</artifactId>
194213
<version>1.5.3</version>
195214
<scope>test</scope>
196215
</dependency>
216+
217+
<dependency>
218+
<groupId>org.assertj</groupId>
219+
<artifactId>assertj-core</artifactId>
220+
<scope>test</scope>
221+
</dependency>
197222
</dependencies>
198223

199224
<build>

cucumber-core/src/main/java/io/cucumber/core/options/PluginOption.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.cucumber.core.plugin.TimelineFormatter;
1717
import io.cucumber.core.plugin.UnusedStepsSummaryPrinter;
1818
import io.cucumber.core.plugin.UsageFormatter;
19+
import io.cucumber.core.plugin.UsageJsonFormatter;
1920
import io.cucumber.plugin.ConcurrentEventListener;
2021
import io.cucumber.plugin.EventListener;
2122
import io.cucumber.plugin.Plugin;
@@ -55,6 +56,7 @@ public class PluginOption implements Options.Plugin {
5556
plugins.put("timeline", TimelineFormatter.class);
5657
plugins.put("unused", UnusedStepsSummaryPrinter.class);
5758
plugins.put("usage", UsageFormatter.class);
59+
plugins.put("usage-json", UsageJsonFormatter.class);
5860
plugins.put("teamcity", TeamCityPlugin.class);
5961
PLUGIN_CLASSES = unmodifiableMap(plugins);
6062
}
Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,54 @@
11
package io.cucumber.core.plugin;
22

3+
import io.cucumber.messages.types.Envelope;
34
import io.cucumber.plugin.ColorAware;
45
import io.cucumber.plugin.ConcurrentEventListener;
56
import io.cucumber.plugin.event.EventPublisher;
6-
import io.cucumber.plugin.event.Status;
7-
import io.cucumber.plugin.event.StepDefinedEvent;
8-
import io.cucumber.plugin.event.TestRunFinished;
9-
import io.cucumber.plugin.event.TestStepFinished;
7+
import io.cucumber.usageformatter.MessagesToUsageWriter;
8+
import io.cucumber.usageformatter.UnusedReportSerializer;
109

10+
import java.io.IOException;
1111
import java.io.OutputStream;
12-
import java.util.Map;
13-
import java.util.Map.Entry;
14-
import java.util.Set;
15-
import java.util.TreeMap;
16-
import java.util.TreeSet;
17-
18-
import static io.cucumber.core.plugin.Formats.ansi;
19-
import static io.cucumber.core.plugin.Formats.monochrome;
20-
import static java.util.Locale.ROOT;
2112

13+
/**
14+
* Formatter to measure performance of steps. Includes average and median step
15+
* duration.
16+
*/
2217
public final class UnusedStepsSummaryPrinter implements ColorAware, ConcurrentEventListener {
2318

24-
private final Map<String, String> registeredSteps = new TreeMap<>();
25-
private final Set<String> usedSteps = new TreeSet<>();
26-
private final UTF8PrintWriter out;
27-
private Formats formats = ansi();
19+
private final MessagesToUsageWriter writer;
2820

21+
@SuppressWarnings("WeakerAccess") // Used by PluginFactory
2922
public UnusedStepsSummaryPrinter(OutputStream out) {
30-
this.out = new UTF8PrintWriter(out);
23+
this.writer = MessagesToUsageWriter.builder(new UnusedReportSerializer())
24+
.build(out);
3125
}
3226

3327
@Override
3428
public void setEventPublisher(EventPublisher publisher) {
35-
// Record any steps registered
36-
publisher.registerHandlerFor(StepDefinedEvent.class, this::handleStepDefinedEvent);
37-
// Remove any steps that run
38-
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
39-
// Print summary when done
40-
publisher.registerHandlerFor(TestRunFinished.class, event -> finishReport());
41-
}
42-
43-
private void handleStepDefinedEvent(StepDefinedEvent event) {
44-
registeredSteps.put(event.getStepDefinition().getLocation(), event.getStepDefinition().getPattern());
45-
}
46-
47-
private void handleTestStepFinished(TestStepFinished event) {
48-
String codeLocation = event.getTestStep().getCodeLocation();
49-
if (codeLocation != null) {
50-
usedSteps.add(codeLocation);
51-
}
29+
publisher.registerHandlerFor(Envelope.class, this::write);
5230
}
5331

54-
private void finishReport() {
55-
// Remove all used steps
56-
usedSteps.forEach(registeredSteps::remove);
57-
58-
if (registeredSteps.isEmpty()) {
59-
return;
32+
private void write(Envelope event) {
33+
try {
34+
writer.write(event);
35+
} catch (IOException e) {
36+
throw new IllegalStateException(e);
6037
}
6138

62-
Format format = formats.get(Status.UNUSED.name().toLowerCase(ROOT));
63-
out.println(format.text(registeredSteps.size() + " Unused steps:"));
64-
65-
// Output results when done
66-
for (Entry<String, String> entry : registeredSteps.entrySet()) {
67-
String location = entry.getKey();
68-
String pattern = entry.getValue();
69-
out.println(format.text(location) + " # " + pattern);
39+
// TODO: Plugins should implement the closable interface
40+
// and be closed by Cucumber
41+
if (event.getTestRunFinished().isPresent()) {
42+
try {
43+
writer.close();
44+
} catch (IOException e) {
45+
throw new IllegalStateException(e);
46+
}
7047
}
71-
72-
out.close();
7348
}
7449

7550
@Override
7651
public void setMonochrome(boolean monochrome) {
77-
formats = monochrome ? monochrome() : ansi();
52+
// no-op, no colors printed
7853
}
79-
8054
}

0 commit comments

Comments
 (0)