Skip to content

Commit 169bdb1

Browse files
committed
Let @TempDir fail fast with File annotated element and non-default file system temp directory
1 parent a0df73c commit 169bdb1

File tree

3 files changed

+180
-41
lines changed

3 files changed

+180
-41
lines changed

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.lang.reflect.Field;
2727
import java.lang.reflect.Parameter;
2828
import java.nio.file.DirectoryNotEmptyException;
29+
import java.nio.file.FileSystems;
2930
import java.nio.file.FileVisitResult;
3031
import java.nio.file.Files;
3132
import java.nio.file.NoSuchFileException;
@@ -145,7 +146,7 @@ private void injectFields(ExtensionContext context, Object testInstance, Class<?
145146
CleanupMode cleanupMode = determineCleanupModeForField(field);
146147
TempDirFactory factory = determineTempDirFactoryForField(field, scope);
147148
makeAccessible(field).set(testInstance,
148-
getPathOrFile(new FieldContext(field), field.getType(), factory, cleanupMode, scope, context));
149+
getPathOrFile(field.getType(), new FieldContext(field), factory, cleanupMode, scope, context));
149150
}
150151
catch (Throwable t) {
151152
throw ExceptionUtils.throwAsUncheckedException(t);
@@ -178,7 +179,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
178179
CleanupMode cleanupMode = determineCleanupModeForParameter(parameterContext);
179180
Scope scope = getScope(extensionContext);
180181
TempDirFactory factory = determineTempDirFactoryForParameter(parameterContext, scope);
181-
return getPathOrFile(parameterContext, parameterType, factory, cleanupMode, scope, extensionContext);
182+
return getPathOrFile(parameterType, parameterContext, factory, cleanupMode, scope, extensionContext);
182183
}
183184

184185
private CleanupMode determineCleanupModeForField(Field field) {
@@ -248,23 +249,24 @@ private void assertSupportedType(String target, Class<?> type) {
248249
}
249250
}
250251

251-
private Object getPathOrFile(AnnotatedElementContext elementContext, Class<?> type, TempDirFactory factory,
252+
private Object getPathOrFile(Class<?> elementType, AnnotatedElementContext elementContext, TempDirFactory factory,
252253
CleanupMode cleanupMode, Scope scope, ExtensionContext extensionContext) {
253254
Namespace namespace = scope == Scope.PER_DECLARATION //
254255
? NAMESPACE.append(elementContext) //
255256
: NAMESPACE;
256257
Path path = extensionContext.getStore(namespace) //
257-
.getOrComputeIfAbsent(KEY, __ -> createTempDir(factory, cleanupMode, elementContext, extensionContext),
258+
.getOrComputeIfAbsent(KEY,
259+
__ -> createTempDir(factory, cleanupMode, elementType, elementContext, extensionContext),
258260
CloseablePath.class) //
259261
.get();
260262

261-
return (type == Path.class) ? path : path.toFile();
263+
return (elementType == Path.class) ? path : path.toFile();
262264
}
263265

264-
static CloseablePath createTempDir(TempDirFactory factory, CleanupMode cleanupMode,
266+
static CloseablePath createTempDir(TempDirFactory factory, CleanupMode cleanupMode, Class<?> elementType,
265267
AnnotatedElementContext elementContext, ExtensionContext extensionContext) {
266268
try {
267-
return new CloseablePath(factory, cleanupMode, elementContext, extensionContext);
269+
return new CloseablePath(factory, cleanupMode, elementType, elementContext, extensionContext);
268270
}
269271
catch (Exception ex) {
270272
throw new ExtensionConfigurationException("Failed to create default temp directory", ex);
@@ -285,8 +287,8 @@ static class CloseablePath implements CloseableResource {
285287
private final CleanupMode cleanupMode;
286288
private final ExtensionContext extensionContext;
287289

288-
private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, AnnotatedElementContext elementContext,
289-
ExtensionContext extensionContext) throws Exception {
290+
private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, Class<?> elementType,
291+
AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception {
290292
this.dir = factory.createTempDirectory(elementContext, extensionContext);
291293
this.factory = factory;
292294
this.cleanupMode = cleanupMode;
@@ -296,6 +298,13 @@ private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, Annotated
296298
close();
297299
throw new PreconditionViolationException("temp directory must be a directory");
298300
}
301+
302+
if (elementType == File.class && !dir.getFileSystem().equals(FileSystems.getDefault())) {
303+
close();
304+
throw new PreconditionViolationException(
305+
"temp directory with non-default file system cannot be injected into " + File.class.getName()
306+
+ " target");
307+
}
299308
}
300309

301310
Path get() {

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java

Lines changed: 114 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
package org.junit.jupiter.engine.extension;
1212

13+
import static com.google.common.jimfs.Configuration.unix;
14+
import static java.lang.annotation.ElementType.METHOD;
15+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
1316
import static java.nio.file.Files.createDirectory;
1417
import static java.nio.file.Files.createFile;
1518
import static java.nio.file.Files.createSymbolicLink;
@@ -18,28 +21,35 @@
1821
import static java.nio.file.Files.deleteIfExists;
1922
import static org.assertj.core.api.Assertions.assertThat;
2023
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
24+
import static org.junit.jupiter.api.condition.OS.WINDOWS;
2125
import static org.junit.jupiter.api.io.CleanupMode.ALWAYS;
2226
import static org.junit.jupiter.api.io.CleanupMode.DEFAULT;
2327
import static org.junit.jupiter.api.io.CleanupMode.NEVER;
2428
import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS;
2529
import static org.mockito.ArgumentMatchers.any;
2630
import static org.mockito.Mockito.mock;
31+
import static org.mockito.Mockito.reset;
2732
import static org.mockito.Mockito.spy;
2833
import static org.mockito.Mockito.verify;
2934
import static org.mockito.Mockito.when;
3035

36+
import java.io.File;
3137
import java.io.IOException;
38+
import java.lang.annotation.Retention;
39+
import java.lang.annotation.Target;
40+
import java.nio.file.FileSystem;
3241
import java.nio.file.Path;
3342
import java.util.Optional;
3443

44+
import com.google.common.jimfs.Jimfs;
45+
3546
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
3647
import org.junit.jupiter.api.AfterEach;
3748
import org.junit.jupiter.api.BeforeEach;
3849
import org.junit.jupiter.api.DisplayName;
3950
import org.junit.jupiter.api.Nested;
4051
import org.junit.jupiter.api.Test;
4152
import org.junit.jupiter.api.condition.DisabledOnOs;
42-
import org.junit.jupiter.api.condition.OS;
4353
import org.junit.jupiter.api.extension.AnnotatedElementContext;
4454
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
4555
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -49,6 +59,8 @@
4959
import org.junit.jupiter.api.io.TempDirFactory;
5060
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
5161
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
62+
import org.junit.jupiter.params.ParameterizedTest;
63+
import org.junit.jupiter.params.provider.ValueSource;
5264
import org.junit.platform.commons.PreconditionViolationException;
5365
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
5466

@@ -65,6 +77,12 @@ class CloseablePathTests extends AbstractJupiterTestEngineTests {
6577

6678
private TempDirectory.CloseablePath closeablePath;
6779

80+
@Target(METHOD)
81+
@Retention(RUNTIME)
82+
@ValueSource(classes = { File.class, Path.class })
83+
private @interface ElementTypeSource {
84+
}
85+
6886
@BeforeEach
6987
void setUpExtensionContext() {
7088
var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), Namespace.GLOBAL);
@@ -95,72 +113,107 @@ void cleanupRoot() throws IOException {
95113
delete(root);
96114
}
97115

98-
@Test
99116
@DisplayName("succeeds if the factory returns a directory")
100-
void factoryReturnsDirectory() throws Exception {
101-
TempDirFactory factory = spy(new Factory(createDirectory(root.resolve("directory"))));
117+
@ParameterizedTest
118+
@ElementTypeSource
119+
void factoryReturnsDirectoryDynamic(Class<?> elementType) throws IOException {
120+
TempDirFactory factory = (elementContext, extensionContext) -> createDirectory(root.resolve("directory"));
102121

103-
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
122+
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext,
123+
extensionContext);
104124
assertThat(closeablePath.get()).isDirectory();
105125

106126
delete(closeablePath.get());
107127
}
108128

109-
@Test
110129
@DisplayName("succeeds if the factory returns a symbolic link to a directory")
111-
@DisabledOnOs(OS.WINDOWS)
112-
void factoryReturnsSymbolicLinkToDirectory() throws Exception {
130+
@ParameterizedTest
131+
@ElementTypeSource
132+
@DisabledOnOs(WINDOWS)
133+
void factoryReturnsSymbolicLinkToDirectory(Class<?> elementType) throws IOException {
113134
Path directory = createDirectory(root.resolve("directory"));
114-
TempDirFactory factory = spy(new Factory(createSymbolicLink(root.resolve("symbolicLink"), directory)));
135+
TempDirFactory factory = (elementContext,
136+
extensionContext) -> createSymbolicLink(root.resolve("symbolicLink"), directory);
115137

116-
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
138+
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext,
139+
extensionContext);
117140
assertThat(closeablePath.get()).isDirectory();
118141

119142
delete(closeablePath.get());
120143
delete(directory);
121144
}
122145

146+
@DisplayName("succeeds if the factory returns a directory on a non-default file system for a Path annotated element")
123147
@Test
148+
void factoryReturnsDirectoryOnNonDefaultFileSystemWithPath() throws IOException {
149+
TempDirFactory factory = spy(new JimfsFactory());
150+
151+
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, Path.class, elementContext, extensionContext);
152+
assertThat(closeablePath.get()).isDirectory();
153+
154+
delete(closeablePath.get());
155+
}
156+
124157
@DisplayName("fails if the factory returns null")
125-
void factoryReturnsNull() throws IOException {
158+
@ParameterizedTest
159+
@ElementTypeSource
160+
void factoryReturnsNull(Class<?> elementType) throws IOException {
126161
TempDirFactory factory = spy(new Factory(null));
127162

128163
assertThatExtensionConfigurationExceptionIsThrownBy(
129-
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
164+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
130165

131166
verify(factory).close();
132167
}
133168

134-
@Test
135169
@DisplayName("fails if the factory returns a file")
136-
void factoryReturnsFile() throws IOException {
170+
@ParameterizedTest
171+
@ElementTypeSource
172+
void factoryReturnsFile(Class<?> elementType) throws IOException {
137173
Path file = createFile(root.resolve("file"));
138174
TempDirFactory factory = spy(new Factory(file));
139175

140176
assertThatExtensionConfigurationExceptionIsThrownBy(
141-
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
177+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
142178

143179
verify(factory).close();
144180
assertThat(file).doesNotExist();
145181
}
146182

147-
@Test
148183
@DisplayName("fails if the factory returns a symbolic link to a file")
149-
@DisabledOnOs(OS.WINDOWS)
150-
void factoryReturnsSymbolicLinkToFile() throws IOException {
184+
@ParameterizedTest
185+
@ElementTypeSource
186+
@DisabledOnOs(WINDOWS)
187+
void factoryReturnsSymbolicLinkToFile(Class<?> elementType) throws IOException {
151188
Path file = createFile(root.resolve("file"));
152189
Path symbolicLink = createSymbolicLink(root.resolve("symbolicLink"), file);
153190
TempDirFactory factory = spy(new Factory(symbolicLink));
154191

155192
assertThatExtensionConfigurationExceptionIsThrownBy(
156-
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
193+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
157194

158195
verify(factory).close();
159196
assertThat(symbolicLink).doesNotExist();
160197

161198
delete(file);
162199
}
163200

201+
@DisplayName("fails if the factory returns a directory on a non-default file system for a File annotated element")
202+
@Test
203+
void factoryReturnsDirectoryOnNonDefaultFileSystemWithFile() throws IOException {
204+
TempDirFactory factory = spy(new JimfsFactory());
205+
206+
assertThatExceptionOfType(ExtensionConfigurationException.class)//
207+
.isThrownBy(() -> TempDirectory.createTempDir(factory, DEFAULT, File.class, elementContext,
208+
extensionContext))//
209+
.withMessage("Failed to create default temp directory")//
210+
.withCauseInstanceOf(PreconditionViolationException.class)//
211+
.havingCause().withMessage("temp directory with non-default file system cannot be injected into "
212+
+ File.class.getName() + " target");
213+
214+
verify(factory).close();
215+
}
216+
164217
// Mockito spying a lambda fails with: VM does not support modification of given type
165218
private record Factory(Path path) implements TempDirFactory {
166219

@@ -171,6 +224,22 @@ public Path createTempDirectory(AnnotatedElementContext elementContext, Extensio
171224

172225
}
173226

227+
private static class JimfsFactory implements TempDirFactory {
228+
229+
private final FileSystem fileSystem = Jimfs.newFileSystem(unix());
230+
231+
@Override
232+
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
233+
throws Exception {
234+
return createDirectory(fileSystem.getPath("/").resolve("directory"));
235+
}
236+
237+
@Override
238+
public void close() throws IOException {
239+
TempDirFactory.super.close();
240+
}
241+
}
242+
174243
private static void assertThatExtensionConfigurationExceptionIsThrownBy(ThrowingCallable callable) {
175244
assertThatExceptionOfType(ExtensionConfigurationException.class)//
176245
.isThrownBy(callable)//
@@ -201,10 +270,13 @@ void cleanupTempDirectory() throws IOException {
201270
deleteIfExists(closeablePath.get());
202271
}
203272

204-
@Test
205273
@DisplayName("is done for a cleanup mode of ALWAYS")
206-
void always() throws IOException {
207-
closeablePath = TempDirectory.createTempDir(factory, ALWAYS, elementContext, extensionContext);
274+
@ParameterizedTest
275+
@ElementTypeSource
276+
void always(Class<?> elementType) throws IOException {
277+
reset(factory);
278+
279+
closeablePath = TempDirectory.createTempDir(factory, ALWAYS, elementType, elementContext, extensionContext);
208280
assertThat(closeablePath.get()).isDirectory();
209281

210282
closeablePath.close();
@@ -213,10 +285,13 @@ void always() throws IOException {
213285
assertThat(closeablePath.get()).doesNotExist();
214286
}
215287

216-
@Test
217288
@DisplayName("is not done for a cleanup mode of NEVER")
218-
void never() throws IOException {
219-
closeablePath = TempDirectory.createTempDir(factory, NEVER, elementContext, extensionContext);
289+
@ParameterizedTest
290+
@ElementTypeSource
291+
void never(Class<?> elementType) throws IOException {
292+
reset(factory);
293+
294+
closeablePath = TempDirectory.createTempDir(factory, NEVER, elementType, elementContext, extensionContext);
220295
assertThat(closeablePath.get()).isDirectory();
221296

222297
closeablePath.close();
@@ -225,12 +300,16 @@ void never() throws IOException {
225300
assertThat(closeablePath.get()).exists();
226301
}
227302

228-
@Test
229303
@DisplayName("is not done for a cleanup mode of ON_SUCCESS, if there is an exception")
230-
void onSuccessWithException() throws IOException {
304+
@ParameterizedTest
305+
@ElementTypeSource
306+
void onSuccessWithException(Class<?> elementType) throws IOException {
307+
reset(factory);
308+
231309
when(extensionContext.getExecutionException()).thenReturn(Optional.of(new Exception()));
232310

233-
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
311+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
312+
extensionContext);
234313
assertThat(closeablePath.get()).isDirectory();
235314

236315
closeablePath.close();
@@ -239,12 +318,16 @@ void onSuccessWithException() throws IOException {
239318
assertThat(closeablePath.get()).exists();
240319
}
241320

242-
@Test
243321
@DisplayName("is done for a cleanup mode of ON_SUCCESS, if there is no exception")
244-
void onSuccessWithNoException() throws IOException {
322+
@ParameterizedTest
323+
@ElementTypeSource
324+
void onSuccessWithNoException(Class<?> elementType) throws IOException {
325+
reset(factory);
326+
245327
when(extensionContext.getExecutionException()).thenReturn(Optional.empty());
246328

247-
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
329+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
330+
extensionContext);
248331
assertThat(closeablePath.get()).isDirectory();
249332

250333
closeablePath.close();

0 commit comments

Comments
 (0)