Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike API for attaching files to test executions #3336

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle.kts
Expand Up @@ -41,7 +41,8 @@ val jupiterProjects by extra(listOf(
).map { it.dependencyProject })

val vintageProjects by extra(listOf(
projects.junitVintageEngine.dependencyProject
projects.junitVintageEngine.dependencyProject,
projects.junitVintageReporting.dependencyProject
))

val mavenizedProjects by extra(platformProjects + jupiterProjects + vintageProjects)
Expand Down
1 change: 1 addition & 0 deletions documentation/documentation.gradle.kts
Expand Up @@ -46,6 +46,7 @@ dependencies {
testImplementation(kotlin("stdlib"))

testImplementation(projects.junitVintageEngine)
testImplementation(projects.junitVintageReporting)
testRuntimeOnly(libs.apiguardian) {
because("it's required to generate API tables")
}
Expand Down
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 = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2"));
testReporter.publishFile(existingFile);

testReporter.publishFile("test3", dir -> {
Path nestedFile = Files.createDirectory(dir).resolve("nested.txt");
Files.write(nestedFile, singletonList("Nested content"));
});
}
}
// end::user_guide[]
@@ -0,0 +1,36 @@
/*
* Copyright 2015-2023 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example.vintage;

import static java.util.Collections.singletonList;

import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.vintage.reporting.TestReporting;

public class VintageTestReportingDemo {

@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

@Rule
public TestReporting testReporting = new TestReporting();

@Test
public void reportFiles() throws Exception {
Path existingFile = Files.write(temporaryFolder.getRoot().toPath().resolve("test.txt"), singletonList("Test"));
testReporting.publishFile(existingFile);
}
}
Comment on lines +23 to +36
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JUnit 4 example

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really clean!

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,36 @@ default void publishEntry(String value) {
this.publishEntry("value", value);
}

/**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a way to "add" a remote file, e.g., via a URI, would be useful.

* Publish the supplied file and attach it to the current test or container.
* <p>
* The file will be copied to the report output directory replacing any
* potentially existing file with the same name.
*
* @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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to move the file by default or at least provide an option to do so.

}

/**
* Publish a file with the supplied name written by the supplied action and
* attach it to the current test or container.
* <p>
* The {@link Path} passed to the supplied action will be relative to the
* report output directory, but it's up to the action to write the file or
* directory.
*
* @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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface shouldn't be implemented by clients but is annotated with @FunctionalInterface so adding another abstract method would, at least theoretically, break backward compatibility. We could decide to ignore that in this case since nobody should have implemented this, go with this pattern, or introduce a separate FileReporter interface that can be injected alongside TestReporter or could extend it and replace it.

}

}
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 @@ -63,8 +63,8 @@ public Optional<String> getArtifactId() {

@Override
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
JupiterConfiguration configuration = new CachingJupiterConfiguration(
new DefaultJupiterConfiguration(discoveryRequest.getConfigurationParameters()));
JupiterConfiguration configuration = new CachingJupiterConfiguration(new DefaultJupiterConfiguration(
discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirProvider()));
JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration);
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor);
return engineDescriptor;
Expand Down
Expand Up @@ -29,6 +29,7 @@
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDirFactory;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.platform.engine.reporting.OutputDirProvider;

/**
* Caching implementation of the {@link JupiterConfiguration} API.
Expand Down Expand Up @@ -125,4 +126,8 @@ public Supplier<TempDirFactory> getDefaultTempDirFactorySupplier() {
key -> delegate.getDefaultTempDirFactorySupplier());
}

@Override
public OutputDirProvider getOutputDirProvider() {
return delegate.getOutputDirProvider();
}
}
Expand Up @@ -32,6 +32,7 @@
import org.junit.platform.commons.util.ClassNamePatternFilterUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.reporting.OutputDirProvider;

/**
* Default implementation of the {@link JupiterConfiguration} API.
Expand Down Expand Up @@ -63,10 +64,13 @@ public class DefaultJupiterConfiguration implements JupiterConfiguration {
new InstantiatingConfigurationParameterConverter<>(TempDirFactory.class, "temp dir factory");

private final ConfigurationParameters configurationParameters;
private final OutputDirProvider outputDirProvider;

public DefaultJupiterConfiguration(ConfigurationParameters configurationParameters) {
public DefaultJupiterConfiguration(ConfigurationParameters configurationParameters,
OutputDirProvider outputDirProvider) {
this.configurationParameters = Preconditions.notNull(configurationParameters,
"ConfigurationParameters must not be null");
this.outputDirProvider = outputDirProvider;
}

@Override
Expand Down Expand Up @@ -141,4 +145,8 @@ public Supplier<TempDirFactory> getDefaultTempDirFactorySupplier() {
return () -> supplier.get().orElse(TempDirFactory.Standard.INSTANCE);
}

@Override
public OutputDirProvider getOutputDirProvider() {
return outputDirProvider;
}
}
Expand Up @@ -27,6 +27,7 @@
import org.junit.jupiter.api.io.TempDirFactory;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.platform.engine.reporting.OutputDirProvider;

/**
* @since 5.4
Expand Down Expand Up @@ -70,4 +71,5 @@ public interface JupiterConfiguration {

Supplier<TempDirFactory> getDefaultTempDirFactorySupplier();

OutputDirProvider getOutputDirProvider();
}
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,17 @@
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.ReportEntry;
import org.junit.platform.engine.support.hierarchical.Node;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
Expand Down Expand Up @@ -102,6 +107,26 @@ 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 {
configuration.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);
}
}

@Override
public Optional<ExtensionContext> getParent() {
return Optional.ofNullable(this.parent);
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);
}
};
}

}