Repository: junit-team/junit5 (language: java)

Issue:
`@TempDir` with `File` annotated elements should fail fast if temp directory file system is not default
`TempDirFactory` implementations based on non-default file systems like [jimfs](https://github.com/google/jimfs) and [memoryfilesystem](https://github.com/marschall/memoryfilesystem) cannot work with `@TempDir` annotated elements of type `File`.

See also the `Path::toFile` [Javadoc](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/Path.html#toFile()):

> **Throws:**
>    `UnsupportedOperationException` - if this `Path` is not associated with the default provider

Right now, a test like [the example in the user guide](https://junit.org/junit5/docs/current/user-guide/#writing-tests-built-in-extensions-TempDirectory), but with `tempDir` of type `File`:

```java
@Test
void test(@TempDir(factory = JimfsTempDirFactory.class) File tempDir) {
    // perform test
}

static class JimfsTempDirFactory implements TempDirFactory {

    private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());

    @Override
    public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws IOException {
        return Files.createTempDirectory(fileSystem.getPath("/"), "junit-");
    }

    @Override
    public void close() throws IOException {
        fileSystem.close();
    }

}
```

fails with:

```
org.junit.jupiter.api.extension.ParameterResolutionException: Failed to resolve parameter [java.io.File arg0] in method [void io.github.scordio.jimfs.junit.jupiter.Tests.test(java.io.File)]

	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.UnsupportedOperationException
	at com.google.common.jimfs.JimfsPath.toFile(JimfsPath.java:386)
	... 2 more
```

I would like to improve the user experience, clearly stating that `File` annotated elements are not supported.

As of today, the factory doesn't have easy access to the type of the annotated element.

In my extension for Jimfs, I've implemented a workaround:

https://github.com/scordio/jimfs-junit-jupiter/blob/ac6f085a89368823e7f515f335eb44f21d981447/src/main/java/io/github/scordio/jimfs/junit/jupiter/JimfsTempDirFactory.java#L86-L107

## Deliverables

I see two possible directions:

1. JUnit owns the error handling and asks the factory if `File` annotated elements are supported
    * This could be done with a new default method in `TempDirFactory`, e.g., `boolean supportsFile()`.
3. The error handling happens in the factory
    * `AnnotatedElementContext` should expose the element type somehow so that the factory can build up the error handling around it without having to cast the result of `getAnnotatedElement()`. For example, a new `Class<?> getAnnotatedElementType()` method could be added to `AnnotatedElementContext`.

My preference goes to option 1 to reduce duplication in each `TempDirFactory` implementation.

I'm happy to contribute once a solution is agreed.

---

Relates to:
* scordio/jimfs-junit-jupiter@99d946e3ca99c807fb70e28e48b6c588027f3e0a
* marschall/memoryfilesystem-junit-provider#2

CC @marschall


Discussion / hints:
Thanks for reporting! I'm wondering if we should instead introduce a `List<Class<?>> getSupportedTypes()` method in case we want to support some hypothetical other type in the future. Thoughts? 
My first feeling about `List<Class<?>> getSupportedTypes()` was that it leaves too much space to the factory, but that's probably not a problem since JUnit would only do `contains` checks on the returned list/set based on the target type.

Do you imagine to always return `Arrays.asList(Path.class, File.class)` (or equivalent) in the default method, and let it evolve whenever JUnit adds more supported types?

One more alternative could be something similar to [`canSupport(Class<?>, Class<?>)`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/ConversionService.html#canConvert(java.lang.Class,java.lang.Class)) of the Spring `ConversionService`, e.g., `supports(Class<?>)`. In this case, the default method would just return `true`.
I'm wondering if we could check the file system of the returned `Path` and throw an exception when it's not the default file system and trying to inject a `File`. Can you imagine a case where that wouldn't be sufficient?
> Can you imagine a case where that wouldn't be sufficient?

No, not really – this approach seems sensible to me! Plus, it's the easiest to adjust if new corner cases arise.

I can start with the implementation unless you see something else that needs to be defined.
No, I think we're good to go! 👍
@marcphilipp I'm struggling with deciding where the responsibility of checking the file system should be implemented:

* In the `getPathOrFile` method:

  https://github.com/junit-team/junit5/blob/b105747923df9d9a85b4426b4cc5e11c4692ff8d/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java#L250-L261

  **Pros**
  * No new parameter needs to be propagated to the `CloseablePath` constructor

  **Cons**
  * The `CloseablePath` instance should be closed in case the file system validation fails
  * Testing to be covered in both `TempDirectoryPerDeclarationTests` and `TempDirectoryPerContextTests`

* In the `CloseablePath` constructor validation:

  https://github.com/junit-team/junit5/blob/b105747923df9d9a85b4426b4cc5e11c4692ff8d/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java#L287-L298

  **Pros**
  * No `CloseablePath` instance is created when a non-default file system is used
  * Testing can be added to `CloseablePathTests`, although with a bit of complexity (e.g., test case matrix via dynamic tests)

  **Cons**
  * A `Class<?> type` parameter needs to be propagated down to the `CloseablePath` constructor

At the moment, I tend to favor the new constructor parameter, but I wanted to have another opinion before going forward.
I think the validation should occur _before_ creating the temporary directory, if possible. Additionally, we need to ensure whenever `TempDirFactory.createTempDirectory` was called,  the corresponding `CloseablePath` is closed so the cleanup is executed. Therefore, I think we should not throw an exception from the `CloseablePath` constructor but validate in the `getPathOrFile` method.
> I think the validation should occur before creating the temporary directory, if possible.

Theoretically, the directory is created as soon as `TempDirFactory.createTempDirectory` returns. To perform the validation before the directory is created, we'd have to introduce some new API for `TempDirFactory` (related to https://github.com/junit-team/junit5/issues/3910#issuecomment-2260559403). Or do you have something else in mind?

> Additionally, we need to ensure whenever `TempDirFactory.createTempDirectory` was called, the corresponding `CloseablePath` is closed so the cleanup is executed.

Indeed, that's already a gap caused by #3901, which I addressed separately in #3911.

> Therefore, I think we should not throw an exception from the `CloseablePath` constructor but validate in the `getPathOrFile` method.

If the `CloseablePath` constructor throws an exception, it would be handled similarly to #3911, i.e., with a try/catch that ensures the underlying factory is also closed in case a `PreconditionViolationException` is thrown by the temp directory validation. This way, no `CloseablePath` instance would be created, and no close handling would need to be in the `getPathOrFile` caller.

Of course, we can push this logic up to `getPathOrFile` and that will require explicit closure of the `CloseablePath`, as you mentioned.

I think both directions can work so happy to follow your preference. I just wanted to bring both options to the table 🙂 
> > I think the validation should occur before creating the temporary directory, if possible.
> 
> Theoretically, the directory is created as soon as `TempDirFactory.createTempDirectory` returns. To perform the validation before the directory is created, we'd have to introduce some new API for `TempDirFactory` (related to [#3910 (comment)](https://github.com/junit-team/junit5/issues/3910#issuecomment-2260559403)). Or do you have something else in mind?

You're right. Let's not introduce a new API at this time.

> Indeed, that's already a gap caused by #3901, which I addressed separately in #3911.
> 
> > Therefore, I think we should not throw an exception from the `CloseablePath` constructor but validate in the `getPathOrFile` method.
> 
> If the `CloseablePath` constructor throws an exception, it would be handled similarly to #3911, i.e., with a try/catch that ensures the underlying factory is also closed in case a `PreconditionViolationException` is thrown by the temp directory validation. This way, no `CloseablePath` instance would be created, and no close handling would need to be in the `getPathOrFile` caller.

Shouldn't we also delete the `Path` returned from `TempDirFactory.createTempDirectory` if it did not throw an exception before closing the factory? In most cases, the latter should take care of the former but that's not necessarily the case.

> Of course, we can push this logic up to `getPathOrFile` and that will require explicit closure of the `CloseablePath`, as you mentioned.
> 
> I think both directions can work so happy to follow your preference. I just wanted to bring both options to the table 🙂

I'll leave the decision where to put the check up to you.
> Shouldn't we also delete the `Path` returned from `TempDirFactory.createTempDirectory` if it did not throw an exception before closing the factory? In most cases, the latter should take care of the former but that's not necessarily the case.

You're absolutely right 🤦 let me revise that in #3911.

Fix patch:
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc
index 56c448b69304..08edb8126cd6 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc
@@ -183,6 +183,9 @@ on GitHub.
   tests, in particular in recent versions of Java that support records.
 * `@TempDir` now fails fast in case `TempDirFactory::createTempDirectory` returns
   `null`, a file, or a symbolic link to a file.
+* `@TempDir` now fails fast in case the annotated target is of type `File` and
+  `TempDirFactory::createTempDirectory` returns a `Path` that does not belong to the
+  default file system.
 
 
 [[release-notes-5.11.0-junit-vintage]]
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java
index de5fd182d6a8..55d9bbfcf56b 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java
@@ -40,11 +40,24 @@
  *
  * <p>The temporary directory is only created if a field in a test class or a
  * parameter in a lifecycle method or test method is annotated with
- * {@code @TempDir}. If the field type or parameter type is neither {@link Path}
- * nor {@link File}, if a field is declared as {@code final}, or if the temporary
- * directory cannot be created, an {@link ExtensionConfigurationException} or a
- * {@link ParameterResolutionException} will be thrown as appropriate. In
- * addition, a {@code ParameterResolutionException} will be thrown for a
+ * {@code @TempDir}.
+ * An {@link ExtensionConfigurationException} or a
+ * {@link ParameterResolutionException} will be thrown in one of the following
+ * cases:
+ *
+ * <ul>
+ * <li>If the field type or parameter type is neither {@link Path} nor
+       {@link File}.</li>
+ * <li>If a field is declared as {@code final}.</li>
+ * <li>If the temporary directory cannot be created.</li>
+ * <li>If the field type or parameter type is {@code File} and a custom
+ *     {@linkplain TempDir#factory() factory} is used, which creates a temporary
+ *     directory that does not belong to the
+ *     {@linkplain java.nio.file.FileSystems#getDefault() default file system}.
+ * </li>
+ * </ul>
+ *
+ * In addition, a {@code ParameterResolutionException} will be thrown for a
  * constructor parameter annotated with {@code @TempDir}.
  *
  * <h2>Scope</h2>
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java
index a4bb8662cb4c..ac5831d88329 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java
@@ -26,6 +26,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Parameter;
 import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileSystems;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
@@ -145,7 +146,7 @@ private void injectFields(ExtensionContext context, Object testInstance, Class<?
 				CleanupMode cleanupMode = determineCleanupModeForField(field);
 				TempDirFactory factory = determineTempDirFactoryForField(field, scope);
 				makeAccessible(field).set(testInstance,
-					getPathOrFile(new FieldContext(field), field.getType(), factory, cleanupMode, scope, context));
+					getPathOrFile(field.getType(), new FieldContext(field), factory, cleanupMode, scope, context));
 			}
 			catch (Throwable t) {
 				throw ExceptionUtils.throwAsUncheckedException(t);
@@ -178,7 +179,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
 		CleanupMode cleanupMode = determineCleanupModeForParameter(parameterContext);
 		Scope scope = getScope(extensionContext);
 		TempDirFactory factory = determineTempDirFactoryForParameter(parameterContext, scope);
-		return getPathOrFile(parameterContext, parameterType, factory, cleanupMode, scope, extensionContext);
+		return getPathOrFile(parameterType, parameterContext, factory, cleanupMode, scope, extensionContext);
 	}
 
 	private CleanupMode determineCleanupModeForField(Field field) {
@@ -248,23 +249,24 @@ private void assertSupportedType(String target, Class<?> type) {
 		}
 	}
 
-	private Object getPathOrFile(AnnotatedElementContext elementContext, Class<?> type, TempDirFactory factory,
+	private Object getPathOrFile(Class<?> elementType, AnnotatedElementContext elementContext, TempDirFactory factory,
 			CleanupMode cleanupMode, Scope scope, ExtensionContext extensionContext) {
 		Namespace namespace = scope == Scope.PER_DECLARATION //
 				? NAMESPACE.append(elementContext) //
 				: NAMESPACE;
 		Path path = extensionContext.getStore(namespace) //
-				.getOrComputeIfAbsent(KEY, __ -> createTempDir(factory, cleanupMode, elementContext, extensionContext),
+				.getOrComputeIfAbsent(KEY,
+					__ -> createTempDir(factory, cleanupMode, elementType, elementContext, extensionContext),
 					CloseablePath.class) //
 				.get();
 
-		return (type == Path.class) ? path : path.toFile();
+		return (elementType == Path.class) ? path : path.toFile();
 	}
 
-	static CloseablePath createTempDir(TempDirFactory factory, CleanupMode cleanupMode,
+	static CloseablePath createTempDir(TempDirFactory factory, CleanupMode cleanupMode, Class<?> elementType,
 			AnnotatedElementContext elementContext, ExtensionContext extensionContext) {
 		try {
-			return new CloseablePath(factory, cleanupMode, elementContext, extensionContext);
+			return new CloseablePath(factory, cleanupMode, elementType, elementContext, extensionContext);
 		}
 		catch (Exception ex) {
 			throw new ExtensionConfigurationException("Failed to create default temp directory", ex);
@@ -285,8 +287,8 @@ static class CloseablePath implements CloseableResource {
 		private final CleanupMode cleanupMode;
 		private final ExtensionContext extensionContext;
 
-		private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, AnnotatedElementContext elementContext,
-				ExtensionContext extensionContext) throws Exception {
+		private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, Class<?> elementType,
+				AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception {
 			this.dir = factory.createTempDirectory(elementContext, extensionContext);
 			this.factory = factory;
 			this.cleanupMode = cleanupMode;
@@ -296,6 +298,13 @@ private CloseablePath(TempDirFactory factory, CleanupMode cleanupMode, Annotated
 				close();
 				throw new PreconditionViolationException("temp directory must be a directory");
 			}
+
+			if (elementType == File.class && !dir.getFileSystem().equals(FileSystems.getDefault())) {
+				close();
+				throw new PreconditionViolationException(
+					"temp directory with non-default file system cannot be injected into " + File.class.getName()
+							+ " target");
+			}
 		}
 
 		Path get() {


Test patch:
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
index 3f76a49c77b0..6a0645975813 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java
@@ -10,6 +10,9 @@
 
 package org.junit.jupiter.engine.extension;
 
+import static com.google.common.jimfs.Configuration.unix;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.nio.file.Files.createDirectory;
 import static java.nio.file.Files.createFile;
 import static java.nio.file.Files.createSymbolicLink;
@@ -18,20 +21,28 @@
 import static java.nio.file.Files.deleteIfExists;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.junit.jupiter.api.condition.OS.WINDOWS;
 import static org.junit.jupiter.api.io.CleanupMode.ALWAYS;
 import static org.junit.jupiter.api.io.CleanupMode.DEFAULT;
 import static org.junit.jupiter.api.io.CleanupMode.NEVER;
 import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.File;
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.nio.file.FileSystem;
 import java.nio.file.Path;
 import java.util.Optional;
 
+import com.google.common.jimfs.Jimfs;
+
 import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -39,7 +50,6 @@
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.DisabledOnOs;
-import org.junit.jupiter.api.condition.OS;
 import org.junit.jupiter.api.extension.AnnotatedElementContext;
 import org.junit.jupiter.api.extension.ExtensionConfigurationException;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -49,6 +59,8 @@
 import org.junit.jupiter.api.io.TempDirFactory;
 import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
 import org.junit.jupiter.engine.execution.NamespaceAwareStore;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.platform.commons.PreconditionViolationException;
 import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
 
@@ -65,6 +77,12 @@ class CloseablePathTests extends AbstractJupiterTestEngineTests {
 
 	private TempDirectory.CloseablePath closeablePath;
 
+	@Target(METHOD)
+	@Retention(RUNTIME)
+	@ValueSource(classes = { File.class, Path.class })
+	private @interface ElementTypeSource {
+	}
+
 	@BeforeEach
 	void setUpExtensionContext() {
 		var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), Namespace.GLOBAL);
@@ -95,65 +113,84 @@ void cleanupRoot() throws IOException {
 			delete(root);
 		}
 
-		@Test
 		@DisplayName("succeeds if the factory returns a directory")
-		void factoryReturnsDirectory() throws Exception {
-			TempDirFactory factory = spy(new Factory(createDirectory(root.resolve("directory"))));
+		@ParameterizedTest
+		@ElementTypeSource
+		void factoryReturnsDirectoryDynamic(Class<?> elementType) throws IOException {
+			TempDirFactory factory = (elementContext, extensionContext) -> createDirectory(root.resolve("directory"));
 
-			closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
+			closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext,
+				extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			delete(closeablePath.get());
 		}
 
-		@Test
 		@DisplayName("succeeds if the factory returns a symbolic link to a directory")
-		@DisabledOnOs(OS.WINDOWS)
-		void factoryReturnsSymbolicLinkToDirectory() throws Exception {
+		@ParameterizedTest
+		@ElementTypeSource
+		@DisabledOnOs(WINDOWS)
+		void factoryReturnsSymbolicLinkToDirectory(Class<?> elementType) throws IOException {
 			Path directory = createDirectory(root.resolve("directory"));
-			TempDirFactory factory = spy(new Factory(createSymbolicLink(root.resolve("symbolicLink"), directory)));
+			TempDirFactory factory = (elementContext,
+					extensionContext) -> createSymbolicLink(root.resolve("symbolicLink"), directory);
 
-			closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext);
+			closeablePath = TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext,
+				extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			delete(closeablePath.get());
 			delete(directory);
 		}
 
+		@DisplayName("succeeds if the factory returns a directory on a non-default file system for a Path annotated element")
 		@Test
+		void factoryReturnsDirectoryOnNonDefaultFileSystemWithPath() throws IOException {
+			TempDirFactory factory = new JimfsFactory();
+
+			closeablePath = TempDirectory.createTempDir(factory, DEFAULT, Path.class, elementContext, extensionContext);
+			assertThat(closeablePath.get()).isDirectory();
+
+			delete(closeablePath.get());
+		}
+
 		@DisplayName("fails if the factory returns null")
-		void factoryReturnsNull() throws IOException {
+		@ParameterizedTest
+		@ElementTypeSource
+		void factoryReturnsNull(Class<?> elementType) throws IOException {
 			TempDirFactory factory = spy(new Factory(null));
 
 			assertThatExtensionConfigurationExceptionIsThrownBy(
-				() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
+				() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
 
 			verify(factory).close();
 		}
 
-		@Test
 		@DisplayName("fails if the factory returns a file")
-		void factoryReturnsFile() throws IOException {
+		@ParameterizedTest
+		@ElementTypeSource
+		void factoryReturnsFile(Class<?> elementType) throws IOException {
 			Path file = createFile(root.resolve("file"));
 			TempDirFactory factory = spy(new Factory(file));
 
 			assertThatExtensionConfigurationExceptionIsThrownBy(
-				() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
+				() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
 
 			verify(factory).close();
 			assertThat(file).doesNotExist();
 		}
 
-		@Test
 		@DisplayName("fails if the factory returns a symbolic link to a file")
-		@DisabledOnOs(OS.WINDOWS)
-		void factoryReturnsSymbolicLinkToFile() throws IOException {
+		@ParameterizedTest
+		@ElementTypeSource
+		@DisabledOnOs(WINDOWS)
+		void factoryReturnsSymbolicLinkToFile(Class<?> elementType) throws IOException {
 			Path file = createFile(root.resolve("file"));
 			Path symbolicLink = createSymbolicLink(root.resolve("symbolicLink"), file);
 			TempDirFactory factory = spy(new Factory(symbolicLink));
 
 			assertThatExtensionConfigurationExceptionIsThrownBy(
-				() -> TempDirectory.createTempDir(factory, DEFAULT, elementContext, extensionContext));
+				() -> TempDirectory.createTempDir(factory, DEFAULT, elementType, elementContext, extensionContext));
 
 			verify(factory).close();
 			assertThat(symbolicLink).doesNotExist();
@@ -161,6 +198,22 @@ void factoryReturnsSymbolicLinkToFile() throws IOException {
 			delete(file);
 		}
 
+		@DisplayName("fails if the factory returns a directory on a non-default file system for a File annotated element")
+		@Test
+		void factoryReturnsDirectoryOnNonDefaultFileSystemWithFile() throws IOException {
+			TempDirFactory factory = spy(new JimfsFactory());
+
+			assertThatExceptionOfType(ExtensionConfigurationException.class)//
+					.isThrownBy(() -> TempDirectory.createTempDir(factory, DEFAULT, File.class, elementContext,
+						extensionContext))//
+					.withMessage("Failed to create default temp directory")//
+					.withCauseInstanceOf(PreconditionViolationException.class)//
+					.havingCause().withMessage("temp directory with non-default file system cannot be injected into "
+							+ File.class.getName() + " target");
+
+			verify(factory).close();
+		}
+
 		// Mockito spying a lambda fails with: VM does not support modification of given type
 		private record Factory(Path path) implements TempDirFactory {
 
@@ -171,6 +224,22 @@ public Path createTempDirectory(AnnotatedElementContext elementContext, Extensio
 
 		}
 
+		private static class JimfsFactory implements TempDirFactory {
+
+			private final FileSystem fileSystem = Jimfs.newFileSystem(unix());
+
+			@Override
+			public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
+					throws Exception {
+				return createDirectory(fileSystem.getPath("/").resolve("directory"));
+			}
+
+			@Override
+			public void close() throws IOException {
+				TempDirFactory.super.close();
+			}
+		}
+
 		private static void assertThatExtensionConfigurationExceptionIsThrownBy(ThrowingCallable callable) {
 			assertThatExceptionOfType(ExtensionConfigurationException.class)//
 					.isThrownBy(callable)//
@@ -201,10 +270,13 @@ void cleanupTempDirectory() throws IOException {
 			deleteIfExists(closeablePath.get());
 		}
 
-		@Test
 		@DisplayName("is done for a cleanup mode of ALWAYS")
-		void always() throws IOException {
-			closeablePath = TempDirectory.createTempDir(factory, ALWAYS, elementContext, extensionContext);
+		@ParameterizedTest
+		@ElementTypeSource
+		void always(Class<?> elementType) throws IOException {
+			reset(factory);
+
+			closeablePath = TempDirectory.createTempDir(factory, ALWAYS, elementType, elementContext, extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			closeablePath.close();
@@ -213,10 +285,13 @@ void always() throws IOException {
 			assertThat(closeablePath.get()).doesNotExist();
 		}
 
-		@Test
 		@DisplayName("is not done for a cleanup mode of NEVER")
-		void never() throws IOException {
-			closeablePath = TempDirectory.createTempDir(factory, NEVER, elementContext, extensionContext);
+		@ParameterizedTest
+		@ElementTypeSource
+		void never(Class<?> elementType) throws IOException {
+			reset(factory);
+
+			closeablePath = TempDirectory.createTempDir(factory, NEVER, elementType, elementContext, extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			closeablePath.close();
@@ -225,12 +300,16 @@ void never() throws IOException {
 			assertThat(closeablePath.get()).exists();
 		}
 
-		@Test
 		@DisplayName("is not done for a cleanup mode of ON_SUCCESS, if there is an exception")
-		void onSuccessWithException() throws IOException {
+		@ParameterizedTest
+		@ElementTypeSource
+		void onSuccessWithException(Class<?> elementType) throws IOException {
+			reset(factory);
+
 			when(extensionContext.getExecutionException()).thenReturn(Optional.of(new Exception()));
 
-			closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
+			closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
+				extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			closeablePath.close();
@@ -239,12 +318,16 @@ void onSuccessWithException() throws IOException {
 			assertThat(closeablePath.get()).exists();
 		}
 
-		@Test
 		@DisplayName("is done for a cleanup mode of ON_SUCCESS, if there is no exception")
-		void onSuccessWithNoException() throws IOException {
+		@ParameterizedTest
+		@ElementTypeSource
+		void onSuccessWithNoException(Class<?> elementType) throws IOException {
+			reset(factory);
+
 			when(extensionContext.getExecutionException()).thenReturn(Optional.empty());
 
-			closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementContext, extensionContext);
+			closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
+				extensionContext);
 			assertThat(closeablePath.get()).isDirectory();
 
 			closeablePath.close();
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerDeclarationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerDeclarationTests.java
index 8c9e455e0041..cfd8fbc452af 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerDeclarationTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerDeclarationTests.java
@@ -321,7 +321,7 @@ void doesNotSupportTempDirFactoryNotReturningDirectory() {
 		@Test
 		@DisplayName("when default @TempDir factory does not return directory")
 		@Order(33)
-		void doesNotSupportCustomDefaultTempDirFactoryReturningNull() {
+		void doesNotSupportCustomDefaultTempDirFactoryNotReturningDirectory() {
 			var results = executeTestsForClassWithDefaultFactory(
 				CustomDefaultFactoryNotReturningDirectoryTestCase.class, FactoryNotReturningDirectory.class);
 
@@ -347,6 +347,28 @@ public Path createTempDirectory(AnnotatedElementContext elementContext, Extensio
 			}
 		}
 
+		@Test
+		@DisplayName("when @TempDir factory returns a non-default file system path for a File annotated element")
+		@Order(34)
+		void doesNotSupportNonDefaultFileSystemTempDirFactoryOnFileAnnotatedElement() {
+			var results = executeTestsForClass(
+				FactoryReturningNonDefaultFileSystemPathForFileAnnotatedElementTestCase.class);
+
+			// @formatter:off
+			assertSingleFailedTest(results, instanceOf(ParameterResolutionException.class),
+					message(m -> m.matches("Failed to resolve parameter \\[.+] in method \\[.+]: .+")),
+					cause(
+							instanceOf(ExtensionConfigurationException.class),
+							message("Failed to create default temp directory"),
+							cause(
+									instanceOf(PreconditionViolationException.class),
+									message("temp directory with non-default file system cannot be injected into "
+											+ File.class.getName() + " target")
+							)
+					));
+			// @formatter:on
+		}
+
 	}
 
 	@Nested
@@ -1424,6 +1446,31 @@ public Path createTempDirectory(AnnotatedElementContext elementContext, Extensio
 
 	}
 
+	static class FactoryReturningNonDefaultFileSystemPathForFileAnnotatedElementTestCase {
+
+		@Test
+		void test(@SuppressWarnings("unused") @TempDir(factory = Factory.class) File tempDir) {
+			// never called
+		}
+
+		private static class Factory implements TempDirFactory {
+
+			private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());
+
+			@Override
+			public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
+					throws Exception {
+				return Files.createTempDirectory(fileSystem.getPath("/"), "prefix");
+			}
+
+			@Override
+			public void close() throws IOException {
+				fileSystem.close();
+			}
+		}
+
+	}
+
 	static class StandardDefaultFactoryTestCase {
 
 		@Test