Skip to content

Commit

Permalink
Spike API for attaching files to test executions
Browse files Browse the repository at this point in the history
  • Loading branch information
marcphilipp committed May 30, 2023
1 parent f77f4b6 commit f18a70e
Show file tree
Hide file tree
Showing 45 changed files with 640 additions and 45 deletions.
18 changes: 18 additions & 0 deletions documentation/src/test/java/example/TestReporterDemo.java
Expand Up @@ -10,11 +10,16 @@

package example;

import static java.util.Collections.singletonList;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

Expand All @@ -41,5 +46,18 @@ void reportMultipleKeyValuePairs(TestReporter testReporter) {
testReporter.publishEntry(values);
}

@Test
void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {

testReporter.publishFile("test1.txt", file -> Files.write(file, singletonList("Test 1")));

Path existingFile = tempDir.resolve("test2.txt");
testReporter.publishFile(Files.write(existingFile, singletonList("Test 2")));

testReporter.publishFile("test3", dir -> {
Path nestedFile = Files.createDirectory(dir).resolve("nested.txt");
Files.write(nestedFile, singletonList("Nested content"));
});
}
}
// end::user_guide[]
Expand Up @@ -10,12 +10,17 @@

package org.junit.jupiter.api;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;

import org.apiguardian.api.API;
import org.junit.jupiter.api.function.ThrowingConsumer;

/**
* Parameters of type {@code TestReporter} can be injected into
Expand Down Expand Up @@ -77,4 +82,34 @@ default void publishEntry(String value) {
this.publishEntry("value", value);
}

/**
* Publish the supplied file and attach it to the current test or container.
* <p>
* The file will be copied to the report output directory.
*
* @param file the file to be attached; never {@code null} or blank
* @since 5.11
*/
@API(status = EXPERIMENTAL, since = "5.11")
default void publishFile(Path file) {
publishFile(file.getFileName().toString(), path -> Files.copy(file, path, REPLACE_EXISTING));
}

/**
* Publish a file with the supplied name written by the supplied action and
* attach it to the current test or container.
* <p>
* The file will be created in the report output directory prior to invoking
* the supplied action.
*
* @param fileName the name of the file to be attached; never {@code null} or blank
* and must not contain any path separators
* @param action the action to be executed to write the file; never {@code null}
* @since 5.11
*/
@API(status = EXPERIMENTAL, since = "5.11")
default void publishFile(String fileName, ThrowingConsumer<Path> action) {
throw new UnsupportedOperationException();
}

}
Expand Up @@ -15,6 +15,7 @@

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -26,6 +27,7 @@

import org.apiguardian.api.API;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.support.ReflectionSupport;
Expand Down Expand Up @@ -365,6 +367,22 @@ default void publishReportEntry(String value) {
this.publishReportEntry("value", value);
}

/**
* Publish a file with the supplied name written by the supplied action and
* attach it to the current test or container.
* <p>
* The file will be located in the report output directory prior to invoking
* the supplied action.
*
* @param fileName the name of the file to be attached; never {@code null} or blank
* and must not contain any path separators
* @param action the action to be executed to write the file; never {@code null}
* @since 5.11
* @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished
*/
@API(status = EXPERIMENTAL, since = "5.11")
void publishFile(String fileName, ThrowingConsumer<Path> action);

/**
* Get the {@link Store} for the supplied {@link Namespace}.
*
Expand Down
Expand Up @@ -82,8 +82,8 @@ protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest

@Override
protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) {
return new JupiterEngineExecutionContext(request.getEngineExecutionListener(),
getJupiterConfiguration(request));
return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), getJupiterConfiguration(request),
request.getOutputDirProvider());
}

/**
Expand Down
Expand Up @@ -13,6 +13,8 @@
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
Expand All @@ -23,14 +25,18 @@
import org.junit.jupiter.api.extension.ExecutableInvoker;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.function.ThrowingConsumer;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.UnrecoverableExceptions;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestTag;
import org.junit.platform.engine.reporting.FileEntry;
import org.junit.platform.engine.reporting.OutputDirProvider;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.hierarchical.Node;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
Expand All @@ -51,11 +57,18 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
private final T testDescriptor;
private final Set<String> tags;
private final JupiterConfiguration configuration;
private final OutputDirProvider outputDirProvider;
private final NamespacedHierarchicalStore<Namespace> valuesStore;
private final ExecutableInvoker executableInvoker;

AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
JupiterConfiguration configuration, ExecutableInvoker executableInvoker) {
this(parent, engineExecutionListener, testDescriptor, configuration, null, executableInvoker);
}

AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
JupiterConfiguration configuration, OutputDirProvider outputDirProvider,
ExecutableInvoker executableInvoker) {
this.executableInvoker = executableInvoker;

Preconditions.notNull(testDescriptor, "TestDescriptor must not be null");
Expand All @@ -65,6 +78,7 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
this.engineExecutionListener = engineExecutionListener;
this.testDescriptor = testDescriptor;
this.configuration = configuration;
this.outputDirProvider = outputDirProvider;
this.valuesStore = createStore(parent);

// @formatter:off
Expand Down Expand Up @@ -102,6 +116,37 @@ public void publishReportEntry(Map<String, String> values) {
this.engineExecutionListener.reportingEntryPublished(this.testDescriptor, ReportEntry.from(values));
}

@Override
public void publishFile(String fileName, ThrowingConsumer<Path> action) {
try {
getOutputDirProvider().createOutputDirectory(this.testDescriptor).ifPresent(dir -> {
try {
Path file = dir.resolve(fileName);
action.accept(file);
this.engineExecutionListener.fileEntryPublished(this.testDescriptor, FileEntry.from(file));
}
catch (Throwable t) {
UnrecoverableExceptions.rethrowIfUnrecoverable(t);
throw new JUnitException("Failed to publish file", t);
}
});
}
catch (IOException e) {
throw new JUnitException("Failed to create output directory", e);
}
}

private OutputDirProvider getOutputDirProvider() {
if (outputDirProvider == null) {
return getParent() //
.filter(it -> it instanceof AbstractExtensionContext) //
.map(it -> (AbstractExtensionContext<?>) it) //
.map(AbstractExtensionContext::getOutputDirProvider).orElseThrow(
() -> new JUnitException("Missing OutputDirProvider"));
}
return outputDirProvider;
}

@Override
public Optional<ExtensionContext> getParent() {
return Optional.ofNullable(this.parent);
Expand Down
Expand Up @@ -55,7 +55,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
EngineExecutionListener executionListener = context.getExecutionListener();
ExecutableInvoker executableInvoker = new DefaultExecutableInvoker(context);
ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionListener, this,
context.getConfiguration(), executableInvoker);
context.getConfiguration(), context.getOutputDirProvider(), executableInvoker);

// @formatter:off
return context.extend()
Expand Down
Expand Up @@ -19,6 +19,7 @@
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.reporting.OutputDirProvider;
import org.junit.platform.engine.support.hierarchical.Node;

/**
Expand All @@ -28,9 +29,9 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext<Jupit

JupiterEngineExtensionContext(EngineExecutionListener engineExecutionListener,
JupiterEngineDescriptor testDescriptor, JupiterConfiguration configuration,
ExecutableInvoker executableInvoker) {
OutputDirProvider outputDirProvider, ExecutableInvoker executableInvoker) {

super(null, engineExecutionListener, testDescriptor, configuration, executableInvoker);
super(null, engineExecutionListener, testDescriptor, configuration, outputDirProvider, executableInvoker);
}

@Override
Expand Down
Expand Up @@ -20,6 +20,7 @@
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.reporting.OutputDirProvider;
import org.junit.platform.engine.support.hierarchical.EngineExecutionContext;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;

Expand All @@ -37,9 +38,9 @@ public class JupiterEngineExecutionContext implements EngineExecutionContext {
private boolean beforeAllCallbacksExecuted = false;
private boolean beforeAllMethodsExecuted = false;

public JupiterEngineExecutionContext(EngineExecutionListener executionListener,
JupiterConfiguration configuration) {
this(new State(executionListener, configuration));
public JupiterEngineExecutionContext(EngineExecutionListener executionListener, JupiterConfiguration configuration,
OutputDirProvider outputDirProvider) {
this(new State(executionListener, configuration, outputDirProvider));
}

private JupiterEngineExecutionContext(State state) {
Expand Down Expand Up @@ -83,6 +84,10 @@ public ThrowableCollector getThrowableCollector() {
return this.state.throwableCollector;
}

public OutputDirProvider getOutputDirProvider() {
return this.state.outputDirProvider;
}

/**
* Track that an attempt was made to execute {@code BeforeAllCallback} extensions.
*
Expand Down Expand Up @@ -124,14 +129,17 @@ private static final class State implements Cloneable {

final EngineExecutionListener executionListener;
final JupiterConfiguration configuration;
final OutputDirProvider outputDirProvider;
TestInstancesProvider testInstancesProvider;
MutableExtensionRegistry extensionRegistry;
ExtensionContext extensionContext;
ThrowableCollector throwableCollector;

State(EngineExecutionListener executionListener, JupiterConfiguration configuration) {
State(EngineExecutionListener executionListener, JupiterConfiguration configuration,
OutputDirProvider outputDirProvider) {
this.executionListener = executionListener;
this.configuration = configuration;
this.outputDirProvider = outputDirProvider;
}

@Override
Expand Down
Expand Up @@ -10,10 +10,14 @@

package org.junit.jupiter.engine.extension;

import java.nio.file.Path;
import java.util.Map;

import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.function.ThrowingConsumer;

/**
* {@link ParameterResolver} that injects a {@link TestReporter}.
Expand All @@ -29,7 +33,17 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon

@Override
public TestReporter resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return extensionContext::publishReportEntry;
return new TestReporter() {
@Override
public void publishEntry(Map<String, String> map) {
extensionContext.publishReportEntry(map);
}

@Override
public void publishFile(String fileName, ThrowingConsumer<Path> action) {
extensionContext.publishFile(fileName, action);
}
};
}

}
Expand Up @@ -43,6 +43,7 @@
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.reporting.OutputDirProvider;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector;
import org.mockito.ArgumentCaptor;
Expand All @@ -54,8 +55,8 @@
* {@link JupiterEngineExtensionContext}, {@link ClassExtensionContext}, and
* {@link MethodExtensionContext}.
*
* @since 5.0
* @see org.junit.jupiter.engine.execution.ExtensionValuesStoreTests
* @since 5.0
*/
public class ExtensionContextTests {

Expand All @@ -74,7 +75,7 @@ void fromJupiterEngineDescriptor() {
UniqueId.root("engine", "junit-jupiter"), configuration);

JupiterEngineExtensionContext engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor,
configuration, null);
configuration, OutputDirProvider.NOOP, null);

// @formatter:off
assertAll("engineContext",
Expand Down Expand Up @@ -161,7 +162,7 @@ void fromMethodTestDescriptor() {
Method testMethod = methodTestDescriptor.getTestMethod();

JupiterEngineExtensionContext engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor,
configuration, null);
configuration, OutputDirProvider.NOOP, null);
ClassExtensionContext classExtensionContext = new ClassExtensionContext(engineExtensionContext, null,
classTestDescriptor, configuration, null, null);
MethodExtensionContext methodExtensionContext = new MethodExtensionContext(classExtensionContext, null,
Expand Down Expand Up @@ -274,7 +275,8 @@ Stream<DynamicTest> configurationParameter() throws Exception {
configuration);

return Stream.of( //
(ExtensionContext) new JupiterEngineExtensionContext(null, engineDescriptor, echo, null), //
(ExtensionContext) new JupiterEngineExtensionContext(null, engineDescriptor, echo, OutputDirProvider.NOOP,
null), //
new ClassExtensionContext(null, null, classTestDescriptor, echo, null, null), //
new MethodExtensionContext(null, null, methodTestDescriptor, echo, null, null) //
).map(context -> dynamicTest(context.getClass().getSimpleName(),
Expand Down

0 comments on commit f18a70e

Please sign in to comment.