Skip to content

Commit 2cdd597

Browse files
scordiomarcphilipp
andauthored
Let @TempDir fail fast if createTempDirectory does not return a directory (#3901)
Resolves #3900. Co-authored-by: Marc Philipp <[email protected]>
1 parent b693945 commit 2cdd597

File tree

7 files changed

+393
-153
lines changed

7 files changed

+393
-153
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ repository on GitHub.
6767
`assertInstanceOf` methods introduced in `5.8` version.
6868
* New generators in `DynamicTest` that take a `Stream`/`Iterator` of `Named<Executable>`
6969
along with a convenient `NamedExecutable` interface that can simplify writing dynamic
70-
tests, in particular in recent version of Java that support records.
70+
tests, in particular in recent versions of Java that support records.
71+
* `@TempDir` now fails fast in case `TempDirFactory::createTempDirectory` returns
72+
`null`, a file, or a symbolic link to a file.
7173

7274

7375
[[release-notes-5.11.0-RC1-junit-vintage]]

documentation/src/test/java/example/TempDirectoryDemo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ static class JimfsTempDirFactory implements TempDirFactory {
137137
@Override
138138
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
139139
throws IOException {
140-
return Files.createTempDirectory(fileSystem.getPath("/"), "junit");
140+
return Files.createTempDirectory(fileSystem.getPath("/"), "junit-");
141141
}
142142

143143
@Override

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,19 @@ static class CloseablePath implements CloseableResource {
284284
private final CleanupMode cleanupMode;
285285
private final ExtensionContext extensionContext;
286286

287-
CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, AnnotatedElementContext elementContext,
287+
private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, AnnotatedElementContext elementContext,
288288
ExtensionContext extensionContext) throws Exception {
289-
this.dir = factory.createTempDirectory(elementContext, extensionContext);
289+
this.dir = validateTempDirectory(factory.createTempDirectory(elementContext, extensionContext));
290290
this.factory = factory;
291291
this.cleanupMode = cleanupMode;
292292
this.extensionContext = extensionContext;
293293
}
294294

295+
private static Path validateTempDirectory(Path dir) {
296+
Preconditions.condition(dir != null && Files.isDirectory(dir), "temp directory must be a directory");
297+
return dir;
298+
}
299+
295300
Path get() {
296301
return dir;
297302
}

junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/CloseablePathCleanupTests.java

Lines changed: 0 additions & 117 deletions
This file was deleted.
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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.engine.extension;
12+
13+
import static java.nio.file.Files.createDirectory;
14+
import static java.nio.file.Files.createFile;
15+
import static java.nio.file.Files.createSymbolicLink;
16+
import static java.nio.file.Files.createTempDirectory;
17+
import static java.nio.file.Files.delete;
18+
import static java.nio.file.Files.deleteIfExists;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
21+
import static org.junit.jupiter.api.io.CleanupMode.ALWAYS;
22+
import static org.junit.jupiter.api.io.CleanupMode.DEFAULT;
23+
import static org.junit.jupiter.api.io.CleanupMode.NEVER;
24+
import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS;
25+
import static org.mockito.ArgumentMatchers.any;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.spy;
28+
import static org.mockito.Mockito.verify;
29+
import static org.mockito.Mockito.when;
30+
31+
import java.io.IOException;
32+
import java.nio.file.Path;
33+
import java.util.Optional;
34+
35+
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
36+
import org.junit.jupiter.api.AfterEach;
37+
import org.junit.jupiter.api.BeforeEach;
38+
import org.junit.jupiter.api.DisplayName;
39+
import org.junit.jupiter.api.Nested;
40+
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.extension.AnnotatedElementContext;
42+
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
43+
import org.junit.jupiter.api.extension.ExtensionContext;
44+
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
45+
import org.junit.jupiter.api.io.CleanupMode;
46+
import org.junit.jupiter.api.io.TempDir;
47+
import org.junit.jupiter.api.io.TempDirFactory;
48+
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
49+
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
50+
import org.junit.platform.commons.PreconditionViolationException;
51+
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
52+
53+
/**
54+
* Integration tests for the creation and cleanup of the {@link TempDirectory}.
55+
*
56+
* @since 5.9
57+
*/
58+
@DisplayName("Temporary directory")
59+
class CloseablePathTests extends AbstractJupiterTestEngineTests {
60+
61+
private final AnnotatedElementContext elementContext = mock();
62+
private final ExtensionContext extensionContext = mock();
63+
64+
private TempDirectory.CloseablePath closeablePath;
65+
66+
@BeforeEach
67+
void setUpExtensionContext() {
68+
var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), Namespace.GLOBAL);
69+
when(extensionContext.getStore(any())).thenReturn(store);
70+
}
71+
72+
/**
73+
* Integration tests for the creation of the {@link TempDirectory} based on the different result
74+
* that {@link TempDirFactory#createTempDirectory(AnnotatedElementContext, ExtensionContext)} may provide.
75+
*
76+
* @since 5.11
77+
*
78+
* @see TempDirFactory
79+
*/
80+
@Nested
81+
@DisplayName("creation")
82+
class Creation {
83+
84+
private Path root;
85+
86+
@BeforeEach
87+
void setUpRootFolder() throws IOException {
88+
root = createTempDirectory("root");
89+
}
90+
91+
@AfterEach
92+
void cleanupRoot() throws IOException {
93+
delete(root);
94+
}
95+
96+
@Test
97+
@DisplayName("succeeds if the factory returns a directory")
98+
void factoryReturnsDirectory() throws Exception {
99+
TempDirFactory factory = (elementContext, extensionContext) -> createDirectory(root.resolve("directory"));
100+
101+
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
102+
assertThat(closeablePath.get()).isDirectory();
103+
104+
delete(closeablePath.get());
105+
}
106+
107+
@Test
108+
@DisplayName("succeeds if the factory returns a symbolic link to a directory")
109+
void factoryReturnsSymbolicLinkToDirectory() throws Exception {
110+
Path directory = createDirectory(root.resolve("directory"));
111+
TempDirFactory factory = (elementContext,
112+
extensionContext) -> createSymbolicLink(root.resolve("symbolicLink"), directory);
113+
114+
closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
115+
assertThat(closeablePath.get()).isDirectory();
116+
117+
delete(closeablePath.get());
118+
delete(directory);
119+
}
120+
121+
@Test
122+
@DisplayName("fails if the factory returns null")
123+
void factoryReturnsNull() {
124+
TempDirFactory factory = (elementContext, extensionContext) -> null;
125+
126+
assertThatExtensionConfigurationExceptionIsThrownBy(
127+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
128+
}
129+
130+
@Test
131+
@DisplayName("fails if the factory returns a file")
132+
void factoryReturnsFile() throws IOException {
133+
Path file = createFile(root.resolve("file"));
134+
TempDirFactory factory = (elementContext, extensionContext) -> file;
135+
136+
assertThatExtensionConfigurationExceptionIsThrownBy(
137+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
138+
139+
delete(file);
140+
}
141+
142+
@Test
143+
@DisplayName("fails if the factory returns a symbolic link to a file")
144+
void factoryReturnsSymbolicLinkToFile() throws IOException {
145+
Path file = createFile(root.resolve("file"));
146+
Path symbolicLink = createSymbolicLink(root.resolve("symbolicLink"), file);
147+
TempDirFactory factory = (elementContext, extensionContext) -> symbolicLink;
148+
149+
assertThatExtensionConfigurationExceptionIsThrownBy(
150+
() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
151+
152+
delete(symbolicLink);
153+
delete(file);
154+
}
155+
156+
private static void assertThatExtensionConfigurationExceptionIsThrownBy(ThrowingCallable callable) {
157+
assertThatExceptionOfType(ExtensionConfigurationException.class)//
158+
.isThrownBy(callable)//
159+
.withMessage("Failed to create default temp directory")//
160+
.withCauseInstanceOf(PreconditionViolationException.class)//
161+
.havingCause().withMessage("temp directory must be a directory");
162+
}
163+
164+
}
165+
166+
/**
167+
* Integration tests for cleanup of the {@link TempDirectory} when the {@link CleanupMode} is
168+
* set to {@link CleanupMode#ALWAYS}, {@link CleanupMode#NEVER}, or {@link CleanupMode#ON_SUCCESS}.
169+
*
170+
* @since 5.9
171+
*
172+
* @see TempDir
173+
* @see CleanupMode
174+
*/
175+
@Nested
176+
@DisplayName("cleanup")
177+
class Cleanup {
178+
179+
private final TempDirFactory factory = spy(TempDirFactory.Standard.INSTANCE);
180+
181+
@AfterEach
182+
void cleanupTempDirectory() throws IOException {
183+
deleteIfExists(closeablePath.get());
184+
}
185+
186+
@Test
187+
@DisplayName("is done for a cleanup mode of ALWAYS")
188+
void always() throws IOException {
189+
closeablePath = TempDirectory.createTempDir(factory, ALWAYS, elementContext, extensionContext);
190+
assertThat(closeablePath.get()).isDirectory();
191+
192+
closeablePath.close();
193+
assertThat(closeablePath.get()).doesNotExist();
194+
verify(factory).close();
195+
}
196+
197+
@Test
198+
@DisplayName("is not done for a cleanup mode of NEVER")
199+
void never() throws IOException {
200+
closeablePath = TempDirectory.createTempDir(factory, NEVER, elementContext, extensionContext);
201+
assertThat(closeablePath.get()).isDirectory();
202+
203+
closeablePath.close();
204+
assertThat(closeablePath.get()).exists();
205+
verify(factory).close();
206+
}
207+
208+
@Test
209+
@DisplayName("is not done for a cleanup mode of ON_SUCCESS, if there is an exception")
210+
void onSuccessWithException() throws IOException {
211+
when(extensionContext.getExecutionException()).thenReturn(Optional.of(new Exception()));
212+
213+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
214+
assertThat(closeablePath.get()).isDirectory();
215+
216+
closeablePath.close();
217+
assertThat(closeablePath.get()).exists();
218+
verify(factory).close();
219+
}
220+
221+
@Test
222+
@DisplayName("is done for a cleanup mode of ON_SUCCESS, if there is no exception")
223+
void onSuccessWithNoException() throws IOException {
224+
when(extensionContext.getExecutionException()).thenReturn(Optional.empty());
225+
226+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
227+
assertThat(closeablePath.get()).isDirectory();
228+
229+
closeablePath.close();
230+
assertThat(closeablePath.get()).doesNotExist();
231+
verify(factory).close();
232+
}
233+
234+
}
235+
236+
}

0 commit comments

Comments
 (0)