Skip to content

Commit 47af824

Browse files
committed
Add mandatory media type for publishing files
1 parent f95e249 commit 47af824

File tree

22 files changed

+716
-162
lines changed

22 files changed

+716
-162
lines changed

documentation/src/test/java/example/TestReporterDemo.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.junit.jupiter.api.Test;
2121
import org.junit.jupiter.api.TestReporter;
22+
import org.junit.jupiter.api.extension.MediaType;
2223
import org.junit.jupiter.api.io.TempDir;
2324
import org.junit.jupiter.api.parallel.Execution;
2425
import org.junit.jupiter.api.parallel.ExecutionMode;
@@ -49,15 +50,21 @@ void reportMultipleKeyValuePairs(TestReporter testReporter) {
4950
@Test
5051
void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {
5152

52-
testReporter.publishFile("test1.txt", file -> Files.write(file, singletonList("Test 1")));
53+
testReporter.publishFile("test1.txt", MediaType.TEXT_PLAIN_UTF_8,
54+
file -> Files.write(file, singletonList("Test 1")));
5355

5456
Path existingFile = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2"));
55-
testReporter.publishFile(existingFile);
57+
testReporter.publishFile(existingFile, MediaType.TEXT_PLAIN_UTF_8);
5658

57-
testReporter.publishFile("test3", dir -> {
58-
Path nestedFile = Files.createDirectory(dir).resolve("nested.txt");
59-
Files.write(nestedFile, singletonList("Nested content"));
59+
testReporter.publishDirectory("test3", dir -> {
60+
Files.write(dir.resolve("nested1.txt"), singletonList("Nested content 1"));
61+
Files.write(dir.resolve("nested2.txt"), singletonList("Nested content 2"));
6062
});
63+
64+
Path existingDir = Files.createDirectory(tempDir.resolve("test4"));
65+
Files.write(existingDir.resolve("nested1.txt"), singletonList("Nested content 1"));
66+
Files.write(existingDir.resolve("nested2.txt"), singletonList("Nested content 2"));
67+
testReporter.publishDirectory(existingDir);
6168
}
6269
}
6370
// end::user_guide[]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1515
import static org.apiguardian.api.API.Status.STABLE;
1616

17+
import java.io.IOException;
18+
import java.io.UncheckedIOException;
1719
import java.nio.file.Files;
1820
import java.nio.file.Path;
1921
import java.util.Collections;
2022
import java.util.Map;
23+
import java.util.stream.Stream;
2124

2225
import org.apiguardian.api.API;
26+
import org.junit.jupiter.api.extension.MediaType;
2327
import org.junit.jupiter.api.function.ThrowingConsumer;
28+
import org.junit.platform.commons.util.Preconditions;
2429

2530
/**
2631
* Parameters of type {@code TestReporter} can be injected into
@@ -88,29 +93,88 @@ default void publishEntry(String value) {
8893
* The file will be copied to the report output directory replacing any
8994
* potentially existing file with the same name.
9095
*
91-
* @param file the file to be attached; never {@code null} or blank
96+
* @param file the file to be attached; never {@code null} or blank
97+
* @param mediaType the media type of the file; never {@code null}; use
98+
* {@link MediaType#APPLICATION_OCTET_STREAM} if unknown
9299
* @since 5.12
93100
*/
94101
@API(status = EXPERIMENTAL, since = "5.12")
95-
default void publishFile(Path file) {
96-
publishFile(file.getFileName().toString(), path -> Files.copy(file, path, REPLACE_EXISTING));
102+
default void publishFile(Path file, MediaType mediaType) {
103+
Preconditions.condition(Files.exists(file), () -> "file must exist: " + file);
104+
Preconditions.condition(Files.isRegularFile(file), () -> "file must be a regular file: " + file);
105+
publishFile(file.getFileName().toString(), mediaType, path -> Files.copy(file, path, REPLACE_EXISTING));
97106
}
98107

99108
/**
100-
* Publish a file with the supplied name written by the supplied action and
101-
* attach it to the current test or container.
109+
* Publish the supplied directory and attach it to the current test or
110+
* container.
111+
* <p>
112+
* The entire directory will be copied to the report output directory
113+
* replacing any potentially existing files with the same name.
114+
*
115+
* @param directory the file to be attached; never {@code null} or blank
116+
* @since 5.12
117+
*/
118+
@API(status = EXPERIMENTAL, since = "5.12")
119+
default void publishDirectory(Path directory) {
120+
Preconditions.condition(Files.exists(directory), () -> "directory must exist: " + directory);
121+
Preconditions.condition(Files.isDirectory(directory), () -> "directory must be a directory: " + directory);
122+
publishDirectory(directory.getFileName().toString(), path -> {
123+
try (Stream<Path> stream = Files.walk(directory)) {
124+
stream.forEach(source -> {
125+
Path destination = path.resolve(directory.relativize(source));
126+
try {
127+
if (Files.isDirectory(source)) {
128+
Files.createDirectories(destination);
129+
}
130+
else {
131+
Files.copy(source, destination, REPLACE_EXISTING);
132+
}
133+
}
134+
catch (IOException e) {
135+
throw new UncheckedIOException("Failed to copy files to the output directory", e);
136+
}
137+
});
138+
}
139+
});
140+
}
141+
142+
/**
143+
* Publish a file or directory with the supplied name and media type written
144+
* by the supplied action and attach it to the current test or container.
145+
* <p>
146+
* The {@link Path} passed to the supplied action will be relative to the
147+
* report output directory, but it's up to the action to write the file.
148+
*
149+
* @param name the name of the file to be attached; never {@code null}
150+
* or blank and must not contain any path separators
151+
* @param mediaType the media type of the file; never {@code null}; use
152+
* {@link MediaType#APPLICATION_OCTET_STREAM} if unknown
153+
* @param action the action to be executed to write the file; never
154+
* {@code null}
155+
* @since 5.12
156+
*/
157+
@API(status = EXPERIMENTAL, since = "5.12")
158+
default void publishFile(String name, MediaType mediaType, ThrowingConsumer<Path> action) {
159+
throw new UnsupportedOperationException();
160+
}
161+
162+
/**
163+
* Publish a directory with the supplied name written by the supplied action
164+
* and attach it to the current test or container.
102165
* <p>
103166
* The {@link Path} passed to the supplied action will be relative to the
104-
* report output directory, but it's up to the action to write the file or
105-
* directory.
167+
* report output directory and point to an existing directory, but it's up
168+
* to the action to write files to it.
106169
*
107-
* @param fileName the name of the file to be attached; never {@code null} or blank
108-
* and must not contain any path separators
109-
* @param action the action to be executed to write the file; never {@code null}
170+
* @param name the name of the directory to be attached; never {@code null}
171+
* or blank and must not contain any path separators
172+
* @param action the action to be executed to write the file; never
173+
* {@code null}
110174
* @since 5.12
111175
*/
112176
@API(status = EXPERIMENTAL, since = "5.12")
113-
default void publishFile(String fileName, ThrowingConsumer<Path> action) {
177+
default void publishDirectory(String name, ThrowingConsumer<Path> action) {
114178
throw new UnsupportedOperationException();
115179
}
116180

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,32 @@ default void publishReportEntry(String value) {
374374
* The file will be resolved in the report output directory prior to
375375
* invoking the supplied action.
376376
*
377-
* @param fileName the name of the file to be attached; never {@code null} or blank
378-
* and must not contain any path separators
379-
* @param action the action to be executed to write the file; never {@code null}
377+
* @param name the name of the file to be attached; never {@code null}
378+
* or blank and must not contain any path separators
379+
* @param mediaType the media type of the file; never {@code null}; use
380+
* {@link MediaType#APPLICATION_OCTET_STREAM} if unknown
381+
* @param action the action to be executed to write the file; never {@code null}
380382
* @since 5.12
381383
* @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished
382384
*/
383385
@API(status = EXPERIMENTAL, since = "5.12")
384-
void publishFile(String fileName, ThrowingConsumer<Path> action);
386+
void publishFile(String name, MediaType mediaType, ThrowingConsumer<Path> action);
387+
388+
/**
389+
* Publish a directory with the supplied name written by the supplied action
390+
* and attach it to the current test or container.
391+
* <p>
392+
* The directory will be resolved and created in the report output directory
393+
* prior to invoking the supplied action.
394+
*
395+
* @param name the name of the directory to be attached; never {@code null}
396+
* or blank and must not contain any path separators
397+
* @param action the action to be executed to write the file; never {@code null}
398+
* @since 5.12
399+
* @see org.junit.platform.engine.EngineExecutionListener#fileEntryPublished
400+
*/
401+
@API(status = EXPERIMENTAL, since = "5.12")
402+
void publishDirectory(String name, ThrowingConsumer<Path> action);
385403

386404
/**
387405
* Get the {@link Store} for the supplied {@link Namespace}.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.extension;
12+
13+
import static java.nio.charset.StandardCharsets.UTF_8;
14+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
15+
16+
import java.nio.charset.Charset;
17+
import java.nio.file.Path;
18+
import java.util.Objects;
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
import org.apiguardian.api.API;
23+
import org.junit.jupiter.api.TestReporter;
24+
import org.junit.jupiter.api.function.ThrowingConsumer;
25+
import org.junit.platform.commons.PreconditionViolationException;
26+
import org.junit.platform.commons.util.Preconditions;
27+
28+
/**
29+
* Represents a media type as defined by
30+
* <a href="https://tools.ietf.org/html/rfc2045">RFC 2045</a>.
31+
*
32+
* @since 5.12
33+
* @see TestReporter#publishFile(Path, MediaType)
34+
* @see TestReporter#publishFile(String, MediaType, ThrowingConsumer)
35+
* @see ExtensionContext#publishFile(String, MediaType, ThrowingConsumer)
36+
*/
37+
@API(status = EXPERIMENTAL, since = "5.12")
38+
public class MediaType {
39+
40+
private static final Pattern PATTERN;
41+
static {
42+
// https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
43+
String whitespace = "[ \t]*";
44+
String token = "[0-9A-Za-z!#$%&'*+.^_`|~-]+";
45+
String quotedString = "\"(?:[^\"\\\\]|\\.)*\"";
46+
String parameter = ";" + whitespace + token + "=" + "(?:" + token + "|" + quotedString + ")";
47+
PATTERN = Pattern.compile(token + "/" + token + "(?:" + whitespace + parameter + ")*");
48+
}
49+
50+
/**
51+
* The {@code text/plain} media type.
52+
*/
53+
public static final MediaType TEXT_PLAIN = create("text", "plain");
54+
55+
/**
56+
* The {@code text/plain; charset=UTF-8} media type.
57+
*/
58+
public static final MediaType TEXT_PLAIN_UTF_8 = create("text", "plain", UTF_8);
59+
60+
/**
61+
* The {@code application/json} media type.
62+
*/
63+
public static final MediaType APPLICATION_JSON = create("application", "json");
64+
65+
/**
66+
* The {@code application/json; charset=UTF-8} media type.
67+
*/
68+
public static final MediaType APPLICATION_JSON_UTF_8 = create("application", "json", UTF_8);
69+
70+
/**
71+
* The {@code application/octet-stream} media type.
72+
*/
73+
public static final MediaType APPLICATION_OCTET_STREAM = create("application", "octet-stream");
74+
75+
/**
76+
* The {@code image/jpeg} media type.
77+
*/
78+
public static final MediaType IMAGE_JPEG = create("image", "jpeg");
79+
80+
/**
81+
* The {@code image/png} media type.
82+
*/
83+
public static final MediaType IMAGE_PNG = create("image", "png");
84+
85+
private final String value;
86+
87+
/**
88+
* Parse the given media type value.
89+
* <p>
90+
* Must be valid according to
91+
* <a href="https://tools.ietf.org/html/rfc2045">RFC 2045</a>.
92+
*
93+
* @param value the media type value to parse; never {@code null}
94+
* @return the parsed media type
95+
* @throws PreconditionViolationException if the value is not a valid media type
96+
*/
97+
public static MediaType parse(String value) {
98+
return new MediaType(value);
99+
}
100+
101+
/**
102+
* Create a media type with the given type and subtype.
103+
*
104+
* @param type the type; never {@code null}
105+
* @param subtype the subtype; never {@code null}
106+
* @return the media type
107+
*/
108+
public static MediaType create(String type, String subtype) {
109+
Preconditions.notNull(type, "type must not be null");
110+
Preconditions.notNull(subtype, "subtype must not be null");
111+
return new MediaType(type + "/" + subtype);
112+
}
113+
114+
/**
115+
* Create a media type with the given type, subtype, and charset.
116+
*
117+
* @param type the type; never {@code null}
118+
* @param subtype the subtype; never {@code null}
119+
* @param charset the charset; never {@code null}
120+
* @return the media type
121+
*/
122+
public static MediaType create(String type, String subtype, Charset charset) {
123+
Preconditions.notNull(type, "type must not be null");
124+
Preconditions.notNull(subtype, "subtype must not be null");
125+
Preconditions.notNull(charset, "charset must not be null");
126+
return new MediaType(type + "/" + subtype + "; charset=" + charset.name());
127+
}
128+
129+
private MediaType(String value) {
130+
Matcher matcher = PATTERN.matcher(Preconditions.notNull(value, "value must not be null"));
131+
Preconditions.condition(matcher.matches(), () -> "Invalid media type: '" + value + "'");
132+
this.value = value;
133+
}
134+
135+
/**
136+
* {@return string representation of this media type}
137+
*/
138+
@Override
139+
public String toString() {
140+
return value;
141+
}
142+
143+
@Override
144+
public boolean equals(Object o) {
145+
if (o == null || getClass() != o.getClass())
146+
return false;
147+
MediaType that = (MediaType) o;
148+
return Objects.equals(this.value, that.value);
149+
}
150+
151+
@Override
152+
public int hashCode() {
153+
return Objects.hashCode(value);
154+
}
155+
}

0 commit comments

Comments
 (0)