diff --git a/docs/docs-nav.yml b/docs/docs-nav.yml
index 51d418c67..1ef31bebb 100644
--- a/docs/docs-nav.yml
+++ b/docs/docs-nav.yml
@@ -10,6 +10,8 @@
url: /docs/range-sources/
- title: "@RepeatFailedTest"
url: /docs/repeat-failed-test/
+ - title: "Report entries"
+ url: /docs/report-entries/
- title: "TempDirectory"
url: /docs/temp-directory/
- title: "Vintage @Test"
diff --git a/docs/report-entries.adoc b/docs/report-entries.adoc
new file mode 100644
index 000000000..b83354de2
--- /dev/null
+++ b/docs/report-entries.adoc
@@ -0,0 +1,80 @@
+:page-title: Report entries
+:page-description: JUnit Jupiter extension to report with annotations.
+
+You can use `@ReportEntry` as a simple way to declaratively add metadata to test methods.
+
+From the https://https://junit.org/junit5/docs/current/user-guide/#writing-tests-dependency-injection[JUnit 5 documentation]:
+
+> In JUnit Jupiter you should use `TestReporter` where you used to print information to `stdout` or `stderr` in JUnit 4.
+> Using `@RunWith(JUnitPlatform.class)` will output all reported entries to `stdout`.
+> In addition, some IDEs print report entries to `stdout` or display them in the user interface for test results.
+
+To see how `@ReportEntry` helps, let's first take a look at the conventional approach and then at this extension.
+
+== Standard Use of `TestReporter`
+
+From the https://https://junit.org/junit5/docs/current/user-guide/#writing-tests-dependency-injection[JUnit 5 documentation]:
+
+> If a constructor or method parameter is of type `TestReporter`, the `TestReporterParameterResolver` will supply an instance of `TestReporter`.
+> The `TestReporter` can be used to publish additional data about the current test run.
+> The data can be consumed via the `reportingEntryPublished()` method in a `TestExecutionListener`, allowing it to be viewed in IDEs or included in reports.
+
+So, you would use it like this:
+
+[source,java]
+----
+@Test
+void reportingTest(TestReporter reporter) {
+ reporter.publishEntry("Hello World!");
+ // YOUR TEST CODE HERE
+}
+----
+
+You can have a look at https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/TestReporter.html[the official documentation for more details].
+
+== With the extension
+
+It can be argued that publishing data about your tests should not be in your test code because it is simply not part of your test.
+It is meta-data.
+
+So it makes sense that you should be able to add that meta-data to your test declaratively with annotations and have JUnit take care of the publishing.
+This is what this extension is for!
+
+You can write...
+
+[source,java]
+----
+@Test
+@ReportEntry("Hello World!")
+void reportingTest() {
+ // YOUR TEST CODE HERE
+}
+----
+
+...and the extension will publish your report entry for you!
+
+You can declare multiple report entries on the same test (the annotation is repeatable).
+
+[source,java]
+----
+@Test
+@ReportEntry("foo")
+@ReportEntry("bar")
+void reportingTest() {
+ // YOUR TEST CODE HERE
+}
+----
+
+Just like `TestReporter::publishEntry` accepts a single string as value or a key/value pair, `@ReportEntry` accepts either a single string as value or a key/value pair:
+
+[source,java]
+----
+@Test
+@ReportEntry(key = "line1", value = "Once upon a midnight dreary")
+@ReportEntry(key = "line2", value = "While I pondered weak and weary")
+void edgarAllanPoe() {
+ // YOUR TEST CODE HERE
+}
+----
+
+Again, just like `TestReporter::publishEntry`, if no key is given it defaults to `"value"` (yes, that's not a mixup).
diff --git a/src/main/java/org/junitpioneer/jupiter/ReportEntries.java b/src/main/java/org/junitpioneer/jupiter/ReportEntries.java
new file mode 100644
index 000000000..7a68ace86
--- /dev/null
+++ b/src/main/java/org/junitpioneer/jupiter/ReportEntries.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2015-2020 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junitpioneer.jupiter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(ReportEntryExtension.class)
+public @interface ReportEntries {
+
+ ReportEntry[] value();
+
+}
diff --git a/src/main/java/org/junitpioneer/jupiter/ReportEntry.java b/src/main/java/org/junitpioneer/jupiter/ReportEntry.java
new file mode 100644
index 000000000..811db3f06
--- /dev/null
+++ b/src/main/java/org/junitpioneer/jupiter/ReportEntry.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2015-2020 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junitpioneer.jupiter;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Publish the specified key-value pair to be consumed by an
+ * {@code org.junit.platform.engine.EngineExecutionListener}
+ * in order to supply additional information to the reporting
+ * infrastructure. This is funtionally identical to calling
+ * {@link org.junit.jupiter.api.extension.ExtensionContext#publishReportEntry(String, String) ExtensionContext::publishReportEntry}
+ * from within the test method.
+ */
+@Repeatable(ReportEntries.class)
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(ReportEntryExtension.class)
+public @interface ReportEntry {
+
+ /**
+ * Specifies the key of the pair that's to be published as a report entry.
+ * Defaults to {@code "value"} and can't be blank.
+ *
+ * @see org.junit.jupiter.api.extension.ExtensionContext#publishReportEntry(String, String) ExtensionContext::publishReportEntry
+ */
+ String key() default "value";
+
+ /**
+ * Specifies the value of the pair that's to be published as a report entry.
+ * Can't be blank.
+ *
+ * @see org.junit.jupiter.api.extension.ExtensionContext#publishReportEntry(String, String) ExtensionContext::publishReportEntry
+ */
+ String value();
+
+}
diff --git a/src/main/java/org/junitpioneer/jupiter/ReportEntryExtension.java b/src/main/java/org/junitpioneer/jupiter/ReportEntryExtension.java
new file mode 100644
index 000000000..f2aa1e43a
--- /dev/null
+++ b/src/main/java/org/junitpioneer/jupiter/ReportEntryExtension.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015-2020 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junitpioneer.jupiter;
+
+import static java.lang.String.format;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionConfigurationException;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+class ReportEntryExtension implements BeforeEachCallback {
+
+ @Override
+ public void beforeEach(ExtensionContext context) throws Exception {
+ Utils
+ .findRepeatableAnnotation(context, ReportEntry.class)
+ .peek(ReportEntryExtension::verifyKeyValueAreNotBlank)
+ .forEach(entry -> context.publishReportEntry(entry.key(), entry.value()));
+ }
+
+ private static void verifyKeyValueAreNotBlank(ReportEntry entry) {
+ if (entry.key().isEmpty() || entry.value().isEmpty()) {
+ String message = "Report entries can't have blank key or value: { key=\"%s\", value=\"%s\" }";
+ throw new ExtensionConfigurationException(format(message, entry.key(), entry.value()));
+ }
+ }
+
+}
diff --git a/src/main/java/org/junitpioneer/jupiter/Utils.java b/src/main/java/org/junitpioneer/jupiter/Utils.java
index 094681638..1a56b10b0 100644
--- a/src/main/java/org/junitpioneer/jupiter/Utils.java
+++ b/src/main/java/org/junitpioneer/jupiter/Utils.java
@@ -58,6 +58,19 @@ public static Optional findAnnotation(ExtensionContext
.orElse(Optional.empty());
}
+ /**
+ * Returns the specified repeatable annotation if it either is either present or
+ * meta-present on the test method belonging to the specified {@code context}.
+ */
+ public static Stream findRepeatableAnnotation(ExtensionContext context,
+ Class annotationType) {
+ return Stream
+ .of(context.getElement(), context.getTestClass().map(Class::getEnclosingClass))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .flatMap(el -> AnnotationSupport.findRepeatableAnnotations(el, annotationType).stream());
+ }
+
/**
* A {@link Collectors#toSet() toSet} collector that throws an {@link IllegalStateException}
* on duplicate elements (according to {@link Object#equals(Object) equals}).
diff --git a/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java b/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
index 68e5b7e1a..6a4e8009d 100644
--- a/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
+++ b/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
@@ -11,10 +11,12 @@
package org.junit.jupiter.engine;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.test.event.ExecutionEventRecorder;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -32,6 +34,10 @@ protected ExecutionEventRecorder executeTestsForClass(Class> testClass) {
return executeTests(request().selectors(selectClass(testClass)).build());
}
+ protected ExecutionEventRecorder executeTestsForMethod(Class> testClass, String methodName) {
+ return executeTests(request().selectors(selectMethod(testClass.getName(), methodName)).build());
+ }
+
protected ExecutionEventRecorder executeTests(LauncherDiscoveryRequest request) {
TestDescriptor testDescriptor = discoverTests(request);
ExecutionEventRecorder eventRecorder = new ExecutionEventRecorder();
@@ -39,6 +45,15 @@ protected ExecutionEventRecorder executeTests(LauncherDiscoveryRequest request)
return eventRecorder;
}
+ protected Throwable getFirstFailuresThrowable(ExecutionEventRecorder recorder) {
+ return recorder
+ .getFailedTestFinishedEvents()
+ .get(0)
+ .getPayload(TestExecutionResult.class)
+ .flatMap(TestExecutionResult::getThrowable)
+ .orElseThrow(AssertionError::new);
+ }
+
protected TestDescriptor discoverTests(LauncherDiscoveryRequest request) {
return engine.discover(request, UniqueId.forEngine(engine.getId()));
}
diff --git a/src/test/java/org/junitpioneer/jupiter/ReportEntryExtensionTest.java b/src/test/java/org/junitpioneer/jupiter/ReportEntryExtensionTest.java
new file mode 100644
index 000000000..f8c41fbe7
--- /dev/null
+++ b/src/test/java/org/junitpioneer/jupiter/ReportEntryExtensionTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2015-2020 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * http://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junitpioneer.jupiter;
+
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
+import org.junit.platform.engine.test.event.ExecutionEvent;
+import org.junit.platform.engine.test.event.ExecutionEventRecorder;
+
+public class ReportEntryExtensionTest extends AbstractJupiterTestEngineTests {
+
+ @Test
+ void explicitKey_keyAndValueAreReported() {
+ ExecutionEventRecorder recorder = executeTestsForMethod(ReportEntriesTest.class, "explicitKey");
+
+ List