From b989fe19fa64ead5078aa80bffcb6e59d6e8d5c1 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 01:06:05 +0100 Subject: [PATCH 1/5] Use snapshot versions for messages and query --- java/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/pom.xml b/java/pom.xml index eb9da09..edd660d 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -60,12 +60,12 @@ io.cucumber messages - [29.0.1,31.0.0) + [31.0.0-SNAPSHOT,32.0.0) io.cucumber query - [14.3.0,15.0.0) + [15.0.0-SNAPSHOT,16.0.0) From 0ebf9062ea673583fe742c3dc83e3b22485f8a4d Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 01:16:41 +0100 Subject: [PATCH 2/5] Setup parent pom and checkstyle --- java/.mvn/jvm.config | 10 ++++++++++ java/pom.xml | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 java/.mvn/jvm.config diff --git a/java/.mvn/jvm.config b/java/.mvn/jvm.config new file mode 100644 index 0000000..32599ce --- /dev/null +++ b/java/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/java/pom.xml b/java/pom.xml index edd660d..37b09bb 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -6,7 +6,7 @@ io.cucumber cucumber-parent - 4.5.0 + 5.0.0-SNAPSHOT usage-formatter @@ -98,4 +98,22 @@ test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + validate + + check + + + + + + From b84d5be553fd9dd22e710e1b62c5553bf756f4d6 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 14:35:20 +0100 Subject: [PATCH 3/5] Use Java 17 --- .../io/cucumber/usageformatter/Durations.java | 15 ++++++++--- .../usageformatter/MessagesToUsageWriter.java | 26 +++++++++--------- .../usageformatter/TableFormatter.java | 4 +++ .../cucumber/usageformatter/UsageReport.java | 11 ++++---- .../usageformatter/UsageReportBuilder.java | 14 +++++----- .../usageformatter/UsageReportSerializer.java | 2 +- .../cucumber/usageformatter/package-info.java | 4 +++ .../usageformatter/DurationsTest.java | 22 +++++++-------- .../MessagesToUsageWriterAcceptanceTest.java | 27 +++++++++---------- .../MessagesToUsageWriterTest.java | 7 +++-- 10 files changed, 74 insertions(+), 58 deletions(-) create mode 100644 java/src/main/java/io/cucumber/usageformatter/package-info.java diff --git a/java/src/main/java/io/cucumber/usageformatter/Durations.java b/java/src/main/java/io/cucumber/usageformatter/Durations.java index 9cc1cc4..c84f6a6 100644 --- a/java/src/main/java/io/cucumber/usageformatter/Durations.java +++ b/java/src/main/java/io/cucumber/usageformatter/Durations.java @@ -2,6 +2,7 @@ import io.cucumber.messages.Convertor; import io.cucumber.usageformatter.UsageReport.Statistics; +import org.jspecify.annotations.Nullable; import java.math.BigDecimal; import java.time.Duration; @@ -9,11 +10,17 @@ import java.util.concurrent.TimeUnit; final class Durations { + + private Durations() { + // utility class + } + + @Nullable static Statistics createStatistics(List durations) { if (durations.isEmpty()) { return null; } - + Duration sum = durations.stream() .reduce(Duration::plus) // Can't happen @@ -21,8 +28,8 @@ static Statistics createStatistics(List durations) { Duration mean = sum.dividedBy(durations.size()); Duration moe95 = calculateMarginOfError95(durations, mean); return new Statistics( - Convertor.toMessage(sum), - Convertor.toMessage(mean), + Convertor.toMessage(sum), + Convertor.toMessage(mean), Convertor.toMessage(moe95) ); } @@ -31,7 +38,7 @@ static Statistics createStatistics(List durations) { * Calculate the margin of error with a 0.95% confidence interval. *

* So assuming a normal distribution, the duration of a step will fall - * within {@code mean ± moe95} with 95% probability. + * within {@code mean ± moe95} with 95% probability. * * @see Wikipedia - Margin of error */ diff --git a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java index 42e7aed..e02680c 100644 --- a/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java +++ b/java/src/main/java/io/cucumber/usageformatter/MessagesToUsageWriter.java @@ -49,19 +49,6 @@ public static Builder builder(Serializer serializer) { return new Builder(serializer); } - public static final class Builder { - private final Serializer serializer; - - private Builder(Serializer serializer) { - this.serializer = requireNonNull(serializer); - } - - public MessagesToUsageWriter build(OutputStream out) { - requireNonNull(out); - return new MessagesToUsageWriter(out, serializer); - } - } - @Override public void close() throws IOException { if (streamClosed) { @@ -79,6 +66,19 @@ public void close() throws IOException { } } + public static final class Builder { + private final Serializer serializer; + + private Builder(Serializer serializer) { + this.serializer = requireNonNull(serializer); + } + + public MessagesToUsageWriter build(OutputStream out) { + requireNonNull(out); + return new MessagesToUsageWriter(out, serializer); + } + } + @FunctionalInterface public interface Serializer { diff --git a/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java b/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java index 0d27c23..529157d 100644 --- a/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java +++ b/java/src/main/java/io/cucumber/usageformatter/TableFormatter.java @@ -6,6 +6,10 @@ final class TableFormatter { + private TableFormatter(){ + // utility class + } + static String format(Table table, boolean[] leftAlignColumn) { StringJoiner joiner = new StringJoiner(lineSeparator(), lineSeparator(), lineSeparator()); int[] longestCellLengthInColumn = findLongestCellLengthInColumn(table); diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java index df7beff..a4ddf8e 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReport.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReport.java @@ -4,6 +4,7 @@ import io.cucumber.messages.types.Location; import io.cucumber.messages.types.SourceReference; import io.cucumber.messages.types.StepDefinitionPattern; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Optional; @@ -28,11 +29,11 @@ public static final class StepDefinitionUsage { private final StepDefinitionPattern pattern; private final SourceReference sourceReference; - private final Statistics duration; + private final @Nullable Statistics duration; private final List matches; StepDefinitionUsage( - StepDefinitionPattern pattern, SourceReference sourceReference, Statistics duration, List matches + StepDefinitionPattern pattern, SourceReference sourceReference, @Nullable Statistics duration, List matches ) { this.pattern = requireNonNull(pattern); this.sourceReference = requireNonNull(sourceReference); @@ -44,7 +45,7 @@ public StepDefinitionPattern getExpression() { return pattern; } - public Statistics getDuration() { + public @Nullable Statistics getDuration() { return duration; } @@ -94,9 +95,9 @@ public static final class StepUsage { private final String text; private final Duration duration; private final String uri; - private final Location location; + private final @Nullable Location location; - StepUsage(String text, Duration duration, String uri, Location location) { + StepUsage(String text, Duration duration, String uri, @Nullable Location location) { this.text = requireNonNull(text); this.duration = requireNonNull(duration); this.uri = requireNonNull(uri); diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java index a1edb61..420dc07 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportBuilder.java @@ -5,6 +5,7 @@ import io.cucumber.messages.types.TestStepFinished; import io.cucumber.query.Query; import io.cucumber.usageformatter.UsageReport.StepUsage; +import org.jspecify.annotations.Nullable; import java.time.Duration; import java.util.ArrayList; @@ -12,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Function; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.groupingBy; @@ -31,7 +31,7 @@ UsageReport build() { Map, List>> testStepsFinishedByStepDefinition = query .findAllTestStepFinished() .stream() - .collect(groupingBy(findUnambiguousStepDefinitionBy(), LinkedHashMap::new, mapping(createStepUsage(), toList()))); + .collect(groupingBy(this::findUnambiguousStepDefinitionBy, LinkedHashMap::new, mapping(this::createStepUsage, toList()))); // Add unused step definitions query.findAllStepDefinitions().stream() @@ -58,7 +58,7 @@ private UsageReport.StepDefinitionUsage createStepDefinitionUsage(StepDefinition ); } - private UsageReport.Statistics createStatistics(List stepUsages) { + private UsageReport.@Nullable Statistics createStatistics(List stepUsages) { List durations = stepUsages.stream() .map(StepUsage::getDuration) .map(Convertor::toDuration) @@ -66,8 +66,8 @@ private UsageReport.Statistics createStatistics(List stepUsages) { return Durations.createStatistics(durations); } - private Function> createStepUsage() { - return testStepFinished -> query.findTestStepBy(testStepFinished) + private Optional createStepUsage(TestStepFinished testStepFinished) { + return query.findTestStepBy(testStepFinished) .flatMap(query::findPickleStepBy) .flatMap(pickleStep -> query .findPickleBy(testStepFinished) @@ -80,8 +80,8 @@ private Function> createStepUsage() { )); } - private Function> findUnambiguousStepDefinitionBy() { - return testStepFinished -> query.findTestStepBy(testStepFinished) + private Optional findUnambiguousStepDefinitionBy(TestStepFinished testStepFinished) { + return query.findTestStepBy(testStepFinished) .flatMap(query::findUnambiguousStepDefinitionBy); } diff --git a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java index fd18923..a94bdfe 100644 --- a/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java +++ b/java/src/main/java/io/cucumber/usageformatter/UsageReportSerializer.java @@ -200,6 +200,6 @@ public enum PlainTextFeature { /** * Include steps using a step definition. */ - INCLUDE_STEPS, + INCLUDE_STEPS } } diff --git a/java/src/main/java/io/cucumber/usageformatter/package-info.java b/java/src/main/java/io/cucumber/usageformatter/package-info.java new file mode 100644 index 0000000..fd22319 --- /dev/null +++ b/java/src/main/java/io/cucumber/usageformatter/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.cucumber.usageformatter; + +import org.jspecify.annotations.NullMarked; diff --git a/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java index a32cb3d..50332dc 100644 --- a/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/DurationsTest.java @@ -17,9 +17,9 @@ public class DurationsTest { void testToBigDecimalSeconds() { assertThat(toBigDecimalSeconds(Duration.ofMillis(0))).isEqualTo(BigDecimal.valueOf(0, 9)); assertThat(toBigDecimalSeconds(Duration.ofMillis(100))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); - assertThat(toBigDecimalSeconds(Duration.ofMillis(1000))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); - assertThat(toBigDecimalSeconds(createDuration(0L, 0L))).isEqualTo(BigDecimal.valueOf(0, 9)); - assertThat(toBigDecimalSeconds(createDuration(0L, 100_000_000L))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); + assertThat(toBigDecimalSeconds(Duration.ofSeconds(1))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); + assertThat(toBigDecimalSeconds(createDuration(0L, 0))).isEqualTo(BigDecimal.valueOf(0, 9)); + assertThat(toBigDecimalSeconds(createDuration(0L, 100_000_000))).isEqualTo(BigDecimal.valueOf(100_000_000, 9)); assertThat(toBigDecimalSeconds(createDuration(1L, 0))).isEqualTo(BigDecimal.valueOf(1_000_000_000, 9)); } @@ -38,10 +38,9 @@ void createStatistics_with_even_number_of_values() { Duration.ofSeconds(4) )); - assertThat(statistics).isNotNull(); - assertThat(statistics.getSum()).isEqualTo(createDuration(8L, 0L)); - assertThat(statistics.getMean()).isEqualTo(createDuration(2L, 0L)); - assertThat(statistics.getMoe95()).isEqualTo(createDuration(1L, 224744871L)); + assertThat(statistics).extracting(Statistics::getSum).isEqualTo(createDuration(8L, 0)); + assertThat(statistics).extracting(Statistics::getMean).isEqualTo(createDuration(2L, 0)); + assertThat(statistics).extracting(Statistics::getMoe95).isEqualTo(createDuration(1L, 224744871)); } @Test @@ -52,13 +51,12 @@ void createStatistics_with_odd_number_of_values() { Duration.ofSeconds(4) )); - assertThat(statistics).isNotNull(); - assertThat(statistics.getSum()).isEqualTo(createDuration(7L, 0L)); - assertThat(statistics.getMean()).isEqualTo(createDuration(2L, 333333333L)); - assertThat(statistics.getMoe95()).isEqualTo(createDuration(1L, 440164599L)); + assertThat(statistics).extracting(Statistics::getSum).isEqualTo(createDuration(7L, 0)); + assertThat(statistics).extracting(Statistics::getMean).isEqualTo(createDuration(2L, 333333333)); + assertThat(statistics).extracting(Statistics::getMoe95).isEqualTo(createDuration(1L, 440164599)); } - private static io.cucumber.messages.types.Duration createDuration(long seconds, long nanos) { + private static io.cucumber.messages.types.Duration createDuration(long seconds, int nanos) { return new io.cucumber.messages.types.Duration(seconds, nanos); } } diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java index 7f7e53d..5b7fce2 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterAcceptanceTest.java @@ -2,7 +2,6 @@ import io.cucumber.messages.NdjsonToMessageIterable; import io.cucumber.messages.types.Envelope; -import io.cucumber.usageformatter.MessagesToUsageWriter.Builder; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -22,20 +21,20 @@ import static io.cucumber.usageformatter.Jackson.OBJECT_MAPPER; import static io.cucumber.usageformatter.Jackson.PRETTY_PRINTER; -import static io.cucumber.usageformatter.MessagesToUsageWriter.builder; -import static java.nio.file.Files.readAllBytes; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; class MessagesToUsageWriterAcceptanceTest { - private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class); + private static final NdjsonToMessageIterable.Deserializer deserializer = json -> OBJECT_MAPPER.readValue(json, Envelope.class); private static final MessagesToUsageWriter.Serializer jsonSerializer = OBJECT_MAPPER.writer(PRETTY_PRINTER)::writeValue; static List acceptance() { - Map formats = new LinkedHashMap<>(); - formats.put("json", builder(jsonSerializer)); - formats.put("unused.txt", builder(new UnusedReportSerializer())); - formats.put("step-definitions.txt", builder(UsageReportSerializer.builder().build())); - formats.put("with-steps.txt", builder(UsageReportSerializer.builder() + Map formats = new LinkedHashMap<>(); + formats.put("json", MessagesToUsageWriter.builder(jsonSerializer)); + formats.put("unused.txt", MessagesToUsageWriter.builder(new UnusedReportSerializer())); + formats.put("step-definitions.txt", MessagesToUsageWriter.builder(UsageReportSerializer.builder().build())); + formats.put("with-steps.txt", MessagesToUsageWriter.builder(UsageReportSerializer.builder() .feature(UsageReportSerializer.PlainTextFeature.INCLUDE_STEPS, true) .maxStepsPerStepDefinition(5) .build())); @@ -59,7 +58,7 @@ private static List getSources() { ); } - private static T writeUsageReport(TestCase testCase, T out, Builder builder) throws IOException { + private static T writeUsageReport(TestCase testCase, T out, MessagesToUsageWriter.Builder builder) throws IOException { try (InputStream in = Files.newInputStream(testCase.source)) { try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) { try (MessagesToUsageWriter writer = builder.build(out)) { @@ -76,7 +75,7 @@ private static T writeUsageReport(TestCase testCase, T @MethodSource("acceptance") void test(TestCase testCase) throws IOException { ByteArrayOutputStream bytes = writeUsageReport(testCase, new ByteArrayOutputStream(), testCase.builder); - assertThat(bytes.toString()).isEqualToIgnoringNewLines(new String(readAllBytes(testCase.expected))); + assertThat(bytes.toString(UTF_8)).isEqualToIgnoringNewLines(Files.readString(testCase.expected)); } @ParameterizedTest @@ -95,18 +94,18 @@ void updateExpectedFiles(TestCase testCase) throws IOException { static class TestCase { private final Path source; private final String format; - private final Builder builder; + private final MessagesToUsageWriter.Builder builder; private final Path expected; private final String name; - TestCase(Path source, String format, Builder builder) { + TestCase(Path source, String format, MessagesToUsageWriter.Builder builder) { this.source = source; this.format = format; this.builder = builder; String fileName = source.getFileName().toString(); this.name = fileName.substring(0, fileName.lastIndexOf(".ndjson")); - this.expected = source.getParent().resolve(name + "." + format); + this.expected = requireNonNull(source.getParent()).resolve(name + "." + format); } @Override diff --git a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java index 945b891..2781a42 100644 --- a/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java +++ b/java/src/test/java/io/cucumber/usageformatter/MessagesToUsageWriterTest.java @@ -3,6 +3,7 @@ import io.cucumber.messages.types.Envelope; import io.cucumber.messages.types.TestRunFinished; import io.cucumber.messages.types.TestRunStarted; +import io.cucumber.messages.types.Timestamp; import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; @@ -40,7 +41,9 @@ void it_throws_when_writing_after_close() throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); MessagesToUsageWriter writer = create(bytes); writer.close(); - assertThrows(IOException.class, () -> writer.write(null)); + assertThrows(IOException.class, () -> writer.write( + Envelope.of(new TestRunStarted(new Timestamp(0L, 0), "")) + )); } @Test @@ -58,7 +61,7 @@ private static String renderAsSummary(Envelope... messages) throws IOException { writer.write(message); } } - return new String(bytes.toByteArray(), UTF_8); + return bytes.toString(UTF_8); } private static MessagesToUsageWriter create(ByteArrayOutputStream bytes) { From c547598cf0d12c3f701c8a97db084a2633036a0a Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 15 Nov 2025 00:27:12 +0100 Subject: [PATCH 4/5] Update versions and polish --- java/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index 37b09bb..118f916 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -10,7 +10,7 @@ usage-formatter - 0.1.1-SNAPSHOT + 0.2.0-SNAPSHOT jar Usage Formatter Writes usage statistics for step definitions From e68db2a356147eaa1433e9776fa95fab6e263b08 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 21 Nov 2025 13:36:19 +0100 Subject: [PATCH 5/5] WIP --- java/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index 118f916..9199115 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -60,7 +60,7 @@ io.cucumber messages - [31.0.0-SNAPSHOT,32.0.0) + [32.0.0-SNAPSHOT,33.0.0) io.cucumber