rawDurations = new ArrayList<>();
-
- for (StepDuration stepDuration : stepDurations) {
- rawDurations.add(stepDuration.duration);
- }
- return rawDurations;
}
/**
- * Calculate the average of a list of duration entries
+ * Creates a usage report for step definitions based on a test run.
+ *
+ * Note: Messages are first collected and only written once the stream is
+ * closed.
*/
- Double calculateAverage(List durationEntries) {
- double sum = 0.0;
- for (Double duration : durationEntries) {
- sum = sum + duration;
- }
- if (sum == 0) {
- return 0.0;
- }
-
- return sum / durationEntries.size();
- }
-
- /**
- * Calculate the median of a list of duration entries
- */
- Double calculateMedian(List durationEntries) {
- if (durationEntries.isEmpty()) {
- return 0.0;
- }
- Collections.sort(durationEntries);
- int middle = durationEntries.size() / 2;
- if (durationEntries.size() % 2 == 1) {
- return durationEntries.get(middle);
- } else {
- double total = durationEntries.get(middle - 1) + durationEntries.get(middle);
- return total / 2;
- }
- }
-
- /**
- * Container of Step Definitions (patterns)
- */
- static class StepDefContainer {
-
- private final String source;
- private final List steps;
-
- StepDefContainer(String source, List steps) {
- this.source = source;
- this.steps = steps;
- }
-
- /**
- * The StepDefinition (pattern)
- */
- public String getSource() {
- return source;
- }
-
- /**
- * A list of Steps
- */
- public List getSteps() {
- return steps;
- }
-
- }
-
- /**
- * Container for usage-entries of steps
- */
- static class StepContainer {
-
- private final String name;
- private final Map aggregatedDurations = new HashMap<>();
- private final List durations = new ArrayList<>();
-
- StepContainer(String name) {
- this.name = name;
- }
-
- public String getName() {
- return name;
- }
-
- void putAllAggregatedDurations(Map aggregatedDurations) {
- this.aggregatedDurations.putAll(aggregatedDurations);
- }
-
- public Map getAggregatedDurations() {
- return aggregatedDurations;
- }
-
- List getDurations() {
- return durations;
- }
-
- }
-
- private static double durationToSeconds(Duration duration) {
- return (double) duration.toNanos() / TimeUnit.SECONDS.toNanos(1);
- }
-
- static class StepDuration {
-
- private final double duration;
- private final String location;
-
- StepDuration(Duration duration, String location) {
- this.duration = durationToSeconds(duration);
- this.location = location;
+ public static final class MessagesToUsageWriter implements AutoCloseable {
+
+ private final OutputStreamWriter out;
+ private final Repository repository = Repository.builder()
+ .feature(INCLUDE_GHERKIN_DOCUMENTS, true)
+ .feature(INCLUDE_STEP_DEFINITIONS, true)
+ .build();
+ private final Query query = new Query(repository);
+ private final Serializer serializer;
+ private boolean streamClosed = false;
+
+ public MessagesToUsageWriter(OutputStream out, Serializer serializer) {
+ this.out = new OutputStreamWriter(
+ requireNonNull(out),
+ StandardCharsets.UTF_8);
+ this.serializer = requireNonNull(serializer);
+ }
+
+ public void write(Envelope envelope) throws IOException {
+ if (streamClosed) {
+ throw new IOException("Stream closed");
+ }
+ repository.update(envelope);
+ }
+
+ 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);
+ }
}
-
- public double getDuration() {
- return duration;
+
+ @Override
+ public void close() throws IOException {
+ if (streamClosed) {
+ return;
+ }
+ try {
+ UsageReportWriter.UsageReport report = new UsageReportWriter(query).createUsageReport();
+ serializer.writeValue(out, report);
+ } finally {
+ try {
+ out.close();
+ } finally {
+ streamClosed = true;
+ }
+ }
}
-
- public String getLocation() {
- return location;
+
+ @FunctionalInterface
+ public interface Serializer {
+
+ void writeValue(Writer writer, Object value) throws IOException;
+
}
-
}
-
}
diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/UsageReportWriter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/UsageReportWriter.java
new file mode 100644
index 0000000000..c97217b8d9
--- /dev/null
+++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/UsageReportWriter.java
@@ -0,0 +1,247 @@
+package io.cucumber.core.plugin;
+
+import io.cucumber.messages.Convertor;
+import io.cucumber.messages.types.Location;
+import io.cucumber.messages.types.PickleStep;
+import io.cucumber.messages.types.SourceReference;
+import io.cucumber.messages.types.StepDefinition;
+import io.cucumber.messages.types.StepDefinitionPattern;
+import io.cucumber.messages.types.TestStepFinished;
+import io.cucumber.query.Query;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static io.cucumber.messages.types.StepDefinitionPatternType.CUCUMBER_EXPRESSION;
+import static java.util.Comparator.naturalOrder;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+
+final class UsageReportWriter {
+
+ private final Query query;
+ private final Function uriFormatter = s -> s;
+ private final SourceReferenceFormatter sourceReferenceFormatter;
+
+ UsageReportWriter(Query query) {
+ this.query = query;
+ this.sourceReferenceFormatter = new SourceReferenceFormatter(uriFormatter);
+ }
+
+ UsageReport createUsageReport() {
+ Map> testStepsFinishedByStepDefinition = query.findAllTestStepFinished()
+ .stream()
+ .collect(groupingBy(findUnambiguousStepDefinitionBy(), LinkedHashMap::new,
+ mapping(createStepDuration(), toList())));
+
+ // Add unused step definitions
+ query.findAllStepDefinitions().forEach(stepDefinition -> testStepsFinishedByStepDefinition
+ .computeIfAbsent(stepDefinition, sd -> new ArrayList<>()));
+
+ List stepDefinitionUsages = testStepsFinishedByStepDefinition.entrySet()
+ .stream()
+ .map(entry -> createStepContainer(entry.getKey(), entry.getValue()))
+ .collect(toList());
+ return new UsageReport(stepDefinitionUsages);
+ }
+
+ private StepDefinitionUsage createStepContainer(StepDefinition stepDefinition, List stepUsages) {
+ Statistics aggregatedDurations = createDurationStatistics(stepUsages);
+ String pattern = stepDefinition.getPattern().getSource();
+ String location = sourceReferenceFormatter.format(stepDefinition.getSourceReference()).orElse("");
+ return new StepDefinitionUsage(pattern, location, aggregatedDurations, stepUsages);
+ }
+
+ private static Statistics createDurationStatistics(List stepUsages) {
+ if (stepUsages.isEmpty()) {
+ return null;
+ }
+ Duration sum = stepUsages.stream()
+ .map(StepUsage::getDuration)
+ .reduce(Duration::plus)
+ // Can't happen
+ .orElse(Duration.ZERO);
+
+ Duration min = stepUsages.stream()
+ .map(StepUsage::getDuration)
+ .min(naturalOrder())
+ // Can't happen
+ .orElse(Duration.ZERO);
+
+ Duration max = stepUsages.stream()
+ .map(StepUsage::getDuration)
+ .max(naturalOrder())
+ // Can't happen
+ .orElse(Duration.ZERO);
+
+ Duration average = sum.dividedBy(stepUsages.size());
+
+ Duration median = getMedian(stepUsages);
+
+ return new Statistics(sum, average, median, min, max);
+ }
+
+ private static Duration getMedian(List stepUsages) {
+ long size = stepUsages.size();
+ long medianItems = size % 2 == 0 ? 2 : 1;
+ long medianIndex = size % 2 == 0 ? (size / 2) - 1 : size / 2;
+ return stepUsages.stream()
+ .map(StepUsage::getDuration)
+ .sorted()
+ .skip(medianIndex)
+ .limit(medianItems)
+ .reduce(Duration::plus)
+ .orElse(Duration.ZERO)
+ .dividedBy(medianItems);
+ }
+
+ private Function createStepDuration() {
+ return testStepFinished -> query
+ .findTestStepBy(testStepFinished)
+ .flatMap(query::findPickleStepBy)
+ .map(pickleStep -> createStepDuration(testStepFinished, pickleStep))
+ .orElseGet(() -> new StepUsage("", Duration.ZERO, ""));
+ }
+
+ private StepUsage createStepDuration(TestStepFinished testStepFinished, PickleStep pickleStep) {
+ String text = pickleStep.getText();
+ String location = findLocationOf(testStepFinished);
+ Duration duration = Convertor.toDuration(testStepFinished.getTestStepResult().getDuration());
+ return new StepUsage(text, duration, location);
+ }
+
+ private String findLocationOf(TestStepFinished testStepFinished) {
+ return query.findPickleBy(testStepFinished)
+ .map(pickle -> uriFormatter.apply(pickle.getUri()) + query.findLocationOf(pickle)
+ .map(Location::getLine)
+ .map(line -> ":" + line)
+ .orElse(""))
+ .orElse("");
+ }
+
+ private Function findUnambiguousStepDefinitionBy() {
+ return testStepFinished -> query.findTestStepBy(testStepFinished)
+ .flatMap(query::findUnambiguousStepDefinitionBy)
+ .orElseGet(UsageReportWriter::createDummyStepDefinition);
+ }
+
+ private static StepDefinition createDummyStepDefinition() {
+ return new StepDefinition("", new StepDefinitionPattern("", CUCUMBER_EXPRESSION), SourceReference.of(""));
+ }
+
+ static final class UsageReport {
+ private final List stepDefinitions;
+
+ UsageReport(List stepDefinitions) {
+ this.stepDefinitions = requireNonNull(stepDefinitions);
+ }
+
+ public List getStepDefinitions() {
+ return stepDefinitions;
+ }
+ }
+
+ /**
+ * Container for usage-entries of steps
+ */
+ static final class StepDefinitionUsage {
+
+ private final String expression;
+ private final String location;
+ private final Statistics duration;
+ private final List steps;
+
+ StepDefinitionUsage(
+ String expression, String location, Statistics duration, List steps
+ ) {
+ this.expression = requireNonNull(expression);
+ this.location = requireNonNull(location);
+ this.duration = duration;
+ this.steps = requireNonNull(steps);
+ }
+
+ public String getExpression() {
+ return expression;
+ }
+
+ public Statistics getDuration() {
+ return duration;
+ }
+
+ public List getSteps() {
+ return steps;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+ }
+
+ static final class Statistics {
+ private final Duration sum;
+ private final Duration average;
+ private final Duration median;
+ private final Duration min;
+ private final Duration max;
+
+ Statistics(Duration sum, Duration average, Duration median, Duration min, Duration max) {
+ this.sum = sum;
+ this.average = average;
+ this.median = median;
+ this.min = min;
+ this.max = max;
+ }
+
+ public Duration getSum() {
+ return sum;
+ }
+
+ public Duration getAverage() {
+ return average;
+ }
+
+ public Duration getMedian() {
+ return median;
+ }
+
+ public Duration getMin() {
+ return min;
+ }
+
+ public Duration getMax() {
+ return max;
+ }
+ }
+
+ static final class StepUsage {
+
+ private final String text;
+ private final Duration duration;
+ private final String location;
+
+ StepUsage(String text, Duration duration, String location) {
+ this.text = requireNonNull(text);
+ this.duration = requireNonNull(duration);
+ this.location = requireNonNull(location);
+ }
+
+ public Duration getDuration() {
+ return duration;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public String getText() {
+ return text;
+ }
+ }
+
+}
diff --git a/cucumber-core/src/test/java/io/cucumber/core/plugin/UsageFormatterTest.java b/cucumber-core/src/test/java/io/cucumber/core/plugin/UsageFormatterTest.java
index 00cc379a35..0323d71bce 100644
--- a/cucumber-core/src/test/java/io/cucumber/core/plugin/UsageFormatterTest.java
+++ b/cucumber-core/src/test/java/io/cucumber/core/plugin/UsageFormatterTest.java
@@ -1,262 +1,270 @@
package io.cucumber.core.plugin;
-import io.cucumber.plugin.event.PickleStepTestStep;
-import io.cucumber.plugin.event.Result;
-import io.cucumber.plugin.event.Status;
-import io.cucumber.plugin.event.TestCase;
-import io.cucumber.plugin.event.TestStep;
-import io.cucumber.plugin.event.TestStepFinished;
+import io.cucumber.core.backend.StubStepDefinition;
+import io.cucumber.core.feature.TestFeatureParser;
+import io.cucumber.core.gherkin.Feature;
+import io.cucumber.core.options.RuntimeOptionsBuilder;
+import io.cucumber.core.runner.StepDurationTimeService;
+import io.cucumber.core.runtime.Runtime;
+import io.cucumber.core.runtime.StubBackendSupplier;
+import io.cucumber.core.runtime.StubFeatureSupplier;
+import io.cucumber.core.runtime.TimeServiceEventBus;
import org.json.JSONException;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
+import java.io.File;
import java.time.Duration;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.UUID;
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.IsEqual.equalTo;
-import static org.hamcrest.number.IsCloseTo.closeTo;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static io.cucumber.core.plugin.PrettyFormatterStepDefinition.oneReference;
+import static io.cucumber.core.plugin.PrettyFormatterStepDefinition.twoReference;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
class UsageFormatterTest {
- public static final double EPSILON = 0.001;
-
@Test
- void resultWithPassedStep() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- TestStep testStep = mockTestStep();
- Result result = new Result(Status.PASSED, Duration.ofMillis(12345L), null);
-
- usageFormatter
- .handleTestStepFinished(new TestStepFinished(Instant.EPOCH, mock(TestCase.class), testStep, result));
-
- Map> usageMap = usageFormatter.usageMap;
- assertThat(usageMap.size(), is(equalTo(1)));
- List durationEntries = usageMap.get("stepDef");
- assertThat(durationEntries.size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getName(), is(equalTo("step")));
- assertThat(durationEntries.get(0).getDurations().size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getDurations().get(0).getDuration(), is(closeTo(12.345, EPSILON)));
- }
-
- private PickleStepTestStep mockTestStep() {
- PickleStepTestStep testStep = mock(PickleStepTestStep.class, Mockito.RETURNS_MOCKS);
- when(testStep.getPattern()).thenReturn("stepDef");
- when(testStep.getStepText()).thenReturn("step");
- return testStep;
- }
-
- @Test
- void resultWithPassedAndFailedStep() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- TestStep testStep = mockTestStep();
-
- Result passed = new Result(Status.PASSED, Duration.ofSeconds(12345L), null);
- usageFormatter
- .handleTestStepFinished(new TestStepFinished(Instant.EPOCH, mock(TestCase.class), testStep, passed));
-
- Result failed = new Result(Status.FAILED, Duration.ZERO, null);
- usageFormatter
- .handleTestStepFinished(new TestStepFinished(Instant.EPOCH, mock(TestCase.class), testStep, failed));
-
- Map> usageMap = usageFormatter.usageMap;
- assertThat(usageMap.size(), is(equalTo(1)));
- List durationEntries = usageMap.get("stepDef");
- assertThat(durationEntries.size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getName(), is(equalTo("step")));
- assertThat(durationEntries.get(0).getDurations().size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getDurations().get(0).getDuration(), is(closeTo(12345.0, EPSILON)));
+ void writes_empty_report() throws JSONException {
+ Feature feature = TestFeatureParser.parse("path/test.feature", "" +
+ "Feature: feature name\n");
+
+ StepDurationTimeService timeService = new StepDurationTimeService(Duration.ofMillis(1000));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Runtime.builder()
+ .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID))
+ .withFeatureSupplier(new StubFeatureSupplier(feature))
+ .withAdditionalPlugins(timeService, new UsageFormatter(out))
+ .withRuntimeOptions(new RuntimeOptionsBuilder().setMonochrome().build())
+ .withBackendSupplier(new StubBackendSupplier())
+ .build()
+ .run();
+
+ String expected = "" +
+ "{\n" +
+ " \"stepDefinitions\": []\n" +
+ "}";
+ assertJsonEquals(expected, out);
}
@Test
- void resultWithZeroDuration() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- TestStep testStep = mockTestStep();
- Result result = new Result(Status.PASSED, Duration.ZERO, null);
-
- usageFormatter
- .handleTestStepFinished(new TestStepFinished(Instant.EPOCH, mock(TestCase.class), testStep, result));
-
- Map> usageMap = usageFormatter.usageMap;
- assertThat(usageMap.size(), is(equalTo(1)));
- List durationEntries = usageMap.get("stepDef");
- assertThat(durationEntries.size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getName(), is(equalTo("step")));
- assertThat(durationEntries.get(0).getDurations().size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getDurations().get(0).getDuration(), is(equalTo(0.0)));
+ void writes_unused_report() throws JSONException {
+ Feature feature = TestFeatureParser.parse("path/test.feature", "" +
+ "Feature: feature name\n" +
+ " Scenario: scenario name\n" +
+ " Given first step\n");
+
+ StepDurationTimeService timeService = new StepDurationTimeService(Duration.ofMillis(1000));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Runtime.builder()
+ .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID))
+ .withFeatureSupplier(new StubFeatureSupplier(feature))
+ .withAdditionalPlugins(timeService, new UsageFormatter(out))
+ .withRuntimeOptions(new RuntimeOptionsBuilder().setMonochrome().build())
+ .withBackendSupplier(new StubBackendSupplier(
+ new StubStepDefinition("first step", oneReference()),
+ new StubStepDefinition("second step", twoReference())))
+ .build()
+ .run();
+
+ String featureFile = new File("").toURI() + "path/test.feature";
+ String expected = "" +
+ "{\n" +
+ " \"stepDefinitions\": [\n" +
+ " {\n" +
+ " \"expression\": \"first step\",\n" +
+ " \"location\": \"io.cucumber.core.plugin.PrettyFormatterStepDefinition.one()\",\n" +
+ " \"duration\": {\n" +
+ " \"sum\": 1.000000000,\n" +
+ " \"average\": 1.000000000,\n" +
+ " \"median\": 1.000000000,\n" +
+ " \"min\": 1.000000000,\n" +
+ " \"max\": 1.000000000\n" +
+ " },\n" +
+ " \"steps\": [\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 1.000000000,\n" +
+ " \"location\": \"path/test.feature:2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"expression\": \"second step\",\n" +
+ " \"location\": \"io.cucumber.core.plugin.PrettyFormatterStepDefinition.two()\",\n" +
+ " \"steps\": []\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ assertJsonEquals(expected.replaceAll("path/test.feature", featureFile), out);
}
- // Note: Duplicate of above test
@Test
- void resultWithNullDuration() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- PickleStepTestStep testStep = mockTestStep();
- Result result = new Result(Status.PASSED, Duration.ZERO, null);
-
- usageFormatter
- .handleTestStepFinished(new TestStepFinished(Instant.EPOCH, mock(TestCase.class), testStep, result));
-
- Map> usageMap = usageFormatter.usageMap;
- assertThat(usageMap.size(), is(equalTo(1)));
- List durationEntries = usageMap.get("stepDef");
- assertThat(durationEntries.size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getName(), is(equalTo("step")));
- assertThat(durationEntries.get(0).getDurations().size(), is(equalTo(1)));
- assertThat(durationEntries.get(0).getDurations().get(0).getDuration(), is(equalTo(0.0)));
+ void writes_usage_report() throws JSONException {
+ Feature feature = TestFeatureParser.parse("path/test.feature", "" +
+ "Feature: feature name\n" +
+ " Scenario: scenario name\n" +
+ " Given first step\n");
+
+ StepDurationTimeService timeService = new StepDurationTimeService(Duration.ofMillis(1000));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Runtime.builder()
+ .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID))
+ .withFeatureSupplier(new StubFeatureSupplier(feature))
+ .withAdditionalPlugins(timeService, new UsageFormatter(out))
+ .withRuntimeOptions(new RuntimeOptionsBuilder().setMonochrome().build())
+ .withBackendSupplier(new StubBackendSupplier(
+ new StubStepDefinition("first step", oneReference())))
+ .build()
+ .run();
+
+ String featureFile = new File("").toURI() + "path/test.feature";
+ String expected = "" +
+ "{" +
+ " \"stepDefinitions\": [\n" +
+ " {\n" +
+ " \"expression\": \"first step\",\n" +
+ " \"location\": \"io.cucumber.core.plugin.PrettyFormatterStepDefinition.one()\",\n" +
+ " \"duration\": {\n" +
+ " \"sum\": 1.000000000,\n" +
+ " \"average\": 1.000000000,\n" +
+ " \"median\": 1.000000000,\n" +
+ " \"min\": 1.000000000,\n" +
+ " \"max\": 1.000000000\n" +
+ " },\n" +
+ " \"steps\": [\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 1.000000000,\n" +
+ " \"location\": \"path/test.feature:2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ assertJsonEquals(expected.replaceAll("path/test.feature", featureFile), out);
}
@Test
- @Disabled("TODO")
- void doneWithoutUsageStatisticStrategies() throws JSONException {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- UsageFormatter.StepContainer stepContainer = new UsageFormatter.StepContainer("a step");
- UsageFormatter.StepDuration stepDuration = new UsageFormatter.StepDuration(Duration.ofNanos(1234567800L),
- "location.feature");
- stepContainer.getDurations().addAll(singletonList(stepDuration));
- usageFormatter.usageMap.put("a (.*)", singletonList(stepContainer));
-
- usageFormatter.finishReport();
-
- String json = "" +
- "[\n" +
- " {\n" +
- " \"source\": \"a (.*)\",\n" +
- " \"steps\": [\n" +
- " {\n" +
- " \"name\": \"a step\",\n" +
- " \"aggregatedDurations\": {\n" +
- " \"median\": 1.2345678,\n" +
- " \"average\": 1.2345678\n" +
+ void writes_usage_with_median() throws JSONException {
+ Feature feature = TestFeatureParser.parse("path/test.feature", "" +
+ "Feature: feature name\n" +
+ " Scenario: scenario 1\n" +
+ " Given first step\n" +
+ " Scenario: scenario 2\n" +
+ " Given first step\n" +
+ " Scenario: scenario 3\n" +
+ " Given first step\n");
+
+ StepDurationTimeService timeService = new StepDurationTimeService(
+ Duration.ofMillis(1000),
+ Duration.ofMillis(2000),
+ Duration.ofMillis(4000));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Runtime.builder()
+ .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID))
+ .withFeatureSupplier(new StubFeatureSupplier(feature))
+ .withAdditionalPlugins(timeService, new UsageFormatter(out))
+ .withRuntimeOptions(new RuntimeOptionsBuilder().setMonochrome().build())
+ .withBackendSupplier(new StubBackendSupplier(
+ new StubStepDefinition("first step", oneReference())))
+ .build()
+ .run();
+
+ String featureFile = new File("").toURI() + "path/test.feature";
+ String expected = "" +
+ "{\n" +
+ " \"stepDefinitions\": [\n" +
+ " {\n" +
+ " \"expression\": \"first step\",\n" +
+ " \"location\": \"io.cucumber.core.plugin.PrettyFormatterStepDefinition.one()\",\n" +
+ " \"duration\": {\n" +
+ " \"sum\": 7.000000000,\n" +
+ " \"average\": 2.333333333,\n" +
+ " \"median\": 2.000000000,\n" +
+ " \"min\": 1.000000000,\n" +
+ " \"max\": 4.000000000\n" +
+ " },\n" +
+ " \"steps\": [\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 1.000000000,\n" +
+ " \"location\": \"path/test.feature:2\"\n" +
" },\n" +
- " \"durations\": [\n" +
- " {\n" +
- " \"duration\": 1.2345678,\n" +
- " \"location\": \"location.feature\"\n" +
- " }\n" +
- " ]\n" +
- " }\n" +
- " ]\n" +
- " }\n" +
- "]";
-
- assertEquals(json, out.toString(), true);
- }
-
- @Test
- @Disabled("TODO")
- void doneWithUsageStatisticStrategies() throws JSONException {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
-
- UsageFormatter.StepContainer stepContainer = new UsageFormatter.StepContainer("a step");
- UsageFormatter.StepDuration stepDuration = new UsageFormatter.StepDuration(Duration.ofNanos(12345678L),
- "location.feature");
- stepContainer.getDurations().addAll(singletonList(stepDuration));
-
- usageFormatter.usageMap.put("a (.*)", singletonList(stepContainer));
-
- usageFormatter.finishReport();
-
- assertThat(out.toString(), containsString("0.012345678"));
- String json = "[\n" +
- " {\n" +
- " \"source\": \"a (.*)\",\n" +
- " \"steps\": [\n" +
- " {\n" +
- " \"name\": \"a step\",\n" +
- " \"aggregatedDurations\": {\n" +
- " \"median\": 0.012345678,\n" +
- " \"average\": 0.012345678\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 2.000000000,\n" +
+ " \"location\": \"path/test.feature:4\"\n" +
" },\n" +
- " \"durations\": [\n" +
- " {\n" +
- " \"duration\": 0.012345678,\n" +
- " \"location\": \"location.feature\"\n" +
- " }\n" +
- " ]\n" +
- " }\n" +
- " ]\n" +
- " }\n" +
- "]";
-
- assertEquals(json, out.toString(), true);
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 4.000000000,\n" +
+ " \"location\": \"path/test.feature:6\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ assertJsonEquals(expected.replaceAll("path/test.feature", featureFile), out);
}
@Test
- void calculateAverageFromList() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter
- .calculateAverage(asList(1.0, 2.0, 3.0));
- assertThat(result, is(closeTo(2.0, EPSILON)));
- }
-
- @Test
- void calculateAverageOf() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter.calculateAverage(asList(1.0, 1.0, 2.0));
- assertThat(result, is(closeTo(1.33, 0.01)));
- }
-
- @Test
- void calculateAverageOfEmptylist() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter.calculateAverage(Collections.emptyList());
- assertThat(result, is(equalTo(0.0)));
- }
-
- @Test
- void calculateMedianOfOddNumberOfEntries() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter
- .calculateMedian(asList(1.0, 2.0, 3.0));
- assertThat(result, is(closeTo(2.0, EPSILON)));
- }
-
- @Test
- void calculateMedianOfEvenNumberOfEntries() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter.calculateMedian(
- asList(1.0, 3.0, 10.0, 5.0));
- assertThat(result, is(closeTo(4.0, EPSILON)));
- }
-
- @Test
- void calculateMedianOf() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter.calculateMedian(asList(2.0, 9.0));
- assertThat(result, is(closeTo(5.5, EPSILON)));
+ void writes_usage_with_median_of_two() throws JSONException {
+ Feature feature = TestFeatureParser.parse("path/test.feature", "" +
+ "Feature: feature name\n" +
+ " Scenario: scenario 1\n" +
+ " Given first step\n" +
+ " Scenario: scenario 2\n" +
+ " Given first step\n");
+
+ StepDurationTimeService timeService = new StepDurationTimeService(
+ Duration.ofMillis(2000),
+ Duration.ofMillis(3000));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Runtime.builder()
+ .withEventBus(new TimeServiceEventBus(timeService, UUID::randomUUID))
+ .withFeatureSupplier(new StubFeatureSupplier(feature))
+ .withAdditionalPlugins(timeService, new UsageFormatter(out))
+ .withRuntimeOptions(new RuntimeOptionsBuilder().setMonochrome().build())
+ .withBackendSupplier(new StubBackendSupplier(
+ new StubStepDefinition("first step", oneReference())))
+ .build()
+ .run();
+
+ String featureFile = new File("").toURI() + "path/test.feature";
+ String expected = "" +
+ "{\n" +
+ " \"stepDefinitions\": [\n" +
+ " {\n" +
+ " \"expression\": \"first step\",\n" +
+ " \"location\": \"io.cucumber.core.plugin.PrettyFormatterStepDefinition.one()\",\n" +
+ " \"duration\": {\n" +
+ " \"sum\": 5.000000000,\n" +
+ " \"average\": 2.500000000,\n" +
+ " \"median\": 2.500000000,\n" +
+ " \"min\": 2.000000000,\n" +
+ " \"max\": 3.000000000\n" +
+ " },\n" +
+ " \"steps\": [\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 2.000000000,\n" +
+ " \"location\": \"path/test.feature:2\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"text\": \"first step\",\n" +
+ " \"duration\": 3.000000000,\n" +
+ " \"location\": \"path/test.feature:4\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ assertJsonEquals(expected.replaceAll("path/test.feature", featureFile), out);
}
- @Test
- void calculateMedianOfEmptyList() {
- OutputStream out = new ByteArrayOutputStream();
- UsageFormatter usageFormatter = new UsageFormatter(out);
- Double result = usageFormatter.calculateMedian(Collections.emptyList());
- assertThat(result, is(equalTo(0.0)));
+ private void assertJsonEquals(String expected, ByteArrayOutputStream actual) throws JSONException {
+ assertEquals(expected, new String(actual.toByteArray(), UTF_8), true);
}
}
diff --git a/cucumber-core/src/test/java/io/cucumber/core/runner/StepDurationTimeService.java b/cucumber-core/src/test/java/io/cucumber/core/runner/StepDurationTimeService.java
index 90eff51c32..289554d536 100644
--- a/cucumber-core/src/test/java/io/cucumber/core/runner/StepDurationTimeService.java
+++ b/cucumber-core/src/test/java/io/cucumber/core/runner/StepDurationTimeService.java
@@ -9,16 +9,19 @@
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.List;
public class StepDurationTimeService extends Clock implements ConcurrentEventListener {
private final ThreadLocal currentInstant = new ThreadLocal<>();
- private final Duration stepDuration;
+ private final List stepDuration;
+ private int currentStepDurationIndex;
private final EventHandler stepStartedHandler = event -> handleTestStepStarted();
- public StepDurationTimeService(Duration stepDuration) {
- this.stepDuration = stepDuration;
+ public StepDurationTimeService(Duration... stepDuration) {
+ this.stepDuration = Arrays.asList(stepDuration);
}
@Override
@@ -28,7 +31,8 @@ public void setEventPublisher(EventPublisher publisher) {
private void handleTestStepStarted() {
Instant timeInstant = instant();
- currentInstant.set(timeInstant.plus(stepDuration));
+ currentInstant.set(timeInstant.plus(stepDuration.get(currentStepDurationIndex)));
+ currentStepDurationIndex = (currentStepDurationIndex + 1) % stepDuration.size();
}
@Override