junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Support ignoring errors during `@TempDir` cleanup

Open strangelookingnerd opened this issue 7 months ago • 23 comments

Overview

There are various circumstances that can cause the cleanup of a temporary directory to fail. Some of these errors may indicate open or leaking file handles, issues in the implementation being tested, or problems in the test itself. Others may occur only occasionally on slow filesystems, race-conditions in asynchronous process and so on.

Currently, a test encountering such a problem will fail with something like:

org.junit.platform.commons.JUnitException: Failed to close extension context
org.junit.platform.commons.JUnitException: Failed to close extension context
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.io.IOException: Failed to delete temp directory C:\Windows\TEMP\junit-6301891027881835454. The following paths could not be deleted (see suppressed exceptions for details): <root>, ws
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
	... 2 more
	Suppressed: java.nio.file.DirectoryNotEmptyException: C:\Windows\TEMP\junit-6301891027881835454
		at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:272)
		at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
		at java.base/java.nio.file.Files.delete(Files.java:1152)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2828)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2882)
		... 13 more
	Suppressed: java.nio.file.DirectoryNotEmptyException: C:\Windows\TEMP\junit-6301891027881835454\ws
		at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:272)
		at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
		at java.base/java.nio.file.Files.delete(Files.java:1152)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2828)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2882)
		... 13 more

In my opinion, this is unfortunate and could also be considered a regression compared to JUnit 4. In JUnit 4, it was possible to work around these kinds of issues using TemporaryFolder#assureDeletion, which allowed ignoring failed deletion of the temporary directory.

As of now, there is no way to bypass these issues in JUnit 5 unless you define @TempDir(cleanup = CleanupMode.NEVER) or avoid using @TempDir altogether - both of which seem undesirable.

See #4549 for a working proposal.

strangelookingnerd avatar May 23 '25 12:05 strangelookingnerd

I can reproduce the same issue(s). It seems in my case to be related to the fact Windows does not appear to always immediately close file handles after Java requests them to be closed. I cannot reproduce on my Linux or MacOS builds, just Windows, and this is very spurious.

[INFO] Stack trace
[INFO] org.junit.platform.commons.JUnitException: Failed to close extension context
	at java.base/java.util.Optional.ifPresent(Optional.java:183)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:274)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1[541](https://github.com/ascopes/protobuf-maven-plugin/actions/runs/15225268219/job/42826508202?pr=674#step:6:542))
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
Caused by: java.io.IOException: Failed to delete temp directory C:\Users\RUNNER~1\AppData\Local\Temp\junit-1260862847092294509. The following paths could not be deleted (see suppressed exceptions for details): <root>, inputs, inputs\archive.jar
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	... 27 more
	Suppressed: java.nio.file.DirectoryNotEmptyException: C:\Users\RUNNER~1\AppData\Local\Temp\junit-1260862847092294509
		at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:271)
		at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
		at java.base/java.nio.file.Files.delete(Files.java:1142)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2743)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2797)
		... 38 more
	Suppressed: java.nio.file.DirectoryNotEmptyException: C:\Users\RUNNER~1\AppData\Local\Temp\junit-1260862847092294509\inputs
		at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:271)
		at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
		at java.base/java.nio.file.Files.delete(Files.java:1142)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2743)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2797)
		... 38 more
	Suppressed: java.nio.file.FileSystemException: C:\Users\RUNNER~1\AppData\Local\Temp\junit-1260862847092294509\inputs\archive.jar: The process cannot access the file because it is being used by another process.

		at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
		at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
		at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
		at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
		at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
		at java.base/java.nio.file.Files.delete(Files.java:1142)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2725)
		at java.base/java.nio.file.Files.walkFileTree(Files.java:2797)
		... 38 more
		Suppressed: java.nio.file.FileSystemException: C:\Users\RUNNER~1\AppData\Local\Temp\junit-1260862847092294509\inputs\archive.jar: The process cannot access the file because it is being used by another process.

			... 46 more

[ERROR] Errors: 
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreNotResolvedWhenTheyDoNotExist(String, Path)[1] � JUnit Failed to close extension context
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreNotResolvedWhenTheyDoNotExist(String, Path)[2] � JUnit Failed to close extension context
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreResolvedWhenTheyExist(String, boolean, Path)[1] � JUnit Failed to close extension context
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreResolvedWhenTheyExist(String, boolean, Path)[2] � JUnit Failed to close extension context
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreResolvedWhenTheyExist(String, boolean, Path)[3] � JUnit Failed to close extension context
[ERROR]   UriResourceFetcherTest.nestedFileUrisAreResolvedWhenTheyExist(String, boolean, Path)[4] � JUnit Failed to close extension context

Pipeline reproducing this: https://github.com/ascopes/protobuf-maven-plugin/actions/runs/15225268219/job/42826508202?pr=674

Test case reproducing this: https://github.com/ascopes/protobuf-maven-plugin/blob/43d19dbe0c6cb98bfb4477d5672b5f159a211565/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/UriResourceFetcherTest.java#L135

In my case, I have observed that backing off for a few dozen milliseconds when this occurs and retrying often fixes the issue, so perhaps retry handling is needed in this case with a backoff in the case that the resource is still in use? The example above was from using @ParameterizedTest but I have seen it on regular @Test cases as well... and it is always on Windows.

For now, ignoring the errors would be much appreciated, as it is resulting in my builds being unstable and I'm having to repeatedly rerun builds that take several minutes to complete.

ascopes avatar May 24 '25 08:05 ascopes

Is it possible to label this as a bug in the meantime?

ascopes avatar May 24 '25 08:05 ascopes

Is it possible to label this as a bug in the meantime?

No, it's not a bug of JUnit per-se.

Non-deletable files (on Windows) usually hint at a resource leak or a non-closed file descriptor in the code under test or the test code itself. Make sure that every file, directory stream, ..., any IO-related resource is closed after usage. IIRC, JUnit tries to release already closed file handles by calling System,.gc() at some point in the cleanup process, in order to help with some cases on Windows.

sormuras avatar May 24 '25 09:05 sormuras

For now, ignoring the errors would be much appreciated, as it is resulting in my builds being unstable and I'm having to repeatedly rerun builds that take several minutes to complete.

Ignoring such errors leads to ignorance, ignorance leads to anger, anger leads to [...] suffering.

And suffer from late(r) or never fixing the underlying issues isn't something we should encourage.

sormuras avatar May 24 '25 09:05 sormuras

Thanks for the info, @sormuras.

In my case, the only resources that are in use in the code under test are those created by URL handlers provided by the JDK. In the case of JarURLConnection it has internal details that may defer the closure of the underlying FileURLConnection InputStream (useCaches being one of those cases).

This is problematic as it results in having to write code to specifically work around standard JDK behaviours to prevent OS-specific crashes within JUnit. For consumers that are working with other libraries in their tests under the hood that they have no control over, this prevents @TempDir usage producing stable test results.

Relying on System.gc as a workaround arguably makes this even more spurious, as tests now rely on the underlying memory pressure and GC implementation as to whether they work or not, meaning practises such as test randomization is a no-go in this situation if stable builds are of importance. As of JDK 17, the System.gc call is documented to only act as an advice, and has no guarantee of doing things in a consistent way.

In these cases, is the advice to avoid using JUnit features and roll our own mechanisms for handling temporary storage between test cases? Or should this be raised on the OpenJDK mailing list instead if we consider this behaviour to be incorrect from the standard library perspective?

ascopes avatar May 24 '25 09:05 ascopes

In my case the snippet that is under test that reproducing this issue is the following:

      var conn = url.openConnection();
      conn.setConnectTimeout(TIMEOUT);
      conn.setReadTimeout(TIMEOUT);
      conn.setAllowUserInteraction(false);
      conn.connect();

      try (
          var responseStream = new BufferedInputStream(conn.getInputStream());
          var fileStream = new SizeAwareBufferedOutputStream(Files.newOutputStream(targetFile))
      ) {
        responseStream.transferTo(fileStream);
      }

where the resource opened by conn.getInputStream() is the one leaving the file descriptor dangling when JUnit attempts to destroy it.

ascopes avatar May 24 '25 09:05 ascopes

Related OpenJDK bug: https://bugs.openjdk.org/browse/JDK-8239054

ascopes avatar May 24 '25 10:05 ascopes

Does the suggested workaround of setting conn.setUseCaches(false) work?

marcphilipp avatar May 24 '25 10:05 marcphilipp

In this case it does, but this assumes any third party libraries using this part of the standard library expose this behaviour to avoid crashing the JUnit runner, which isn't ideal as there then no way to consistently intercept this issue without avoiding the JUnit APIs for this.

My concern is that it results in JUnit actively not supporting testing of components using certain parts of the standard library under default settings (one area being classloading).

One such case is URLClassLoader at https://github.com/openjdk/jdk/blob/e89330579d5f38e282512211711fffeeea3e899e/src/java.base/share/classes/java/net/URLClassLoader.java#L222 which would result in test success depending on whether the GC chooses to run or not. Close APIs for this are not implemented on ClassLoader itself, so this relies on every implementation of code that consumes any kind of classloader performing explicit checks to see if it is a specific implementation.

ascopes avatar May 24 '25 10:05 ascopes

Could you please try if adding a ServiceLoader-registered TestExecutionListener like the following works, too?

import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;

import java.net.URLConnection;

import static org.junit.jupiter.api.condition.OS.WINDOWS;

public class FileHandleCacheDisablingTestExecutionListener implements TestExecutionListener {

    @Override
    public void testPlanExecutionStarted(TestPlan testPlan) {
        if (WINDOWS.isCurrentOs()) {
            URLConnection.setDefaultUseCaches("file", false);
            URLConnection.setDefaultUseCaches("jar", false);
            URLConnection.setDefaultUseCaches("zip", false);
        }
    }
}

marcphilipp avatar May 24 '25 10:05 marcphilipp

Sure, will take a look when I am at my PC again in a little bit

ascopes avatar May 24 '25 10:05 ascopes

Seems to fix it!

https://github.com/ascopes/protobuf-maven-plugin/actions/runs/15226250551/job/42828629435

ascopes avatar May 24 '25 10:05 ascopes

Also related issues:

Mind the comment made Daniel Fuchs:

Two possible workaround:

  1. disable caching on the JarURLConnection before opening the stream.
  2. disable caching globally for the jar: protocol

This is related to JDK-8132359 and JDK-8232854 and need to be examined in conjunction with these other issues.

And more links are recorded in https://github.com/microsoft/java-wdb/issues/31

  • https://blogs.oracle.com/quinn/addressing-locked-jar-problems
  • https://sormuras.github.io/blog/2019-05-26-jdk-module-layer-class-loader
  • @JornVernee 's report of IntelliJ IDEA locking jrt-fs.jar on Windows, but not so running IDEA in WSL

sormuras avatar May 24 '25 11:05 sormuras

Regarding the workaround, would the jrt URLStreamHandler also be impacted by this potentially?

ascopes avatar May 24 '25 11:05 ascopes

@strangelookingnerd Would https://github.com/junit-team/junit5/issues/4567#issuecomment-2906759494 also be a viable workaround for you?

marcphilipp avatar May 24 '25 17:05 marcphilipp

@strangelookingnerd Would https://github.com/junit-team/junit5/issues/4567#issuecomment-2906759494 also be a viable workaround for you?

Not really. For context, over the last 6 months I migrated quite a lot of projects (mostly Jenkins related but also some others) from JUnit3 or 4 to the lastet version. See https://github.com/pulls?&q=Migrate+to+JUnit5+is%3Apr+author%3Astrangelookingnerd for some examples. One issue (or rather regression?) I encountered oftentimes is the one described here. Tests suddenly fail because a temporary file can not be deleted.

And suffer from late(r) or never fixing the underlying issues isn't something we should encourage.

While I mostly agree with @sormuras points, I would not agree that the requested feature in any way promotes writing bad tests.

Let me try to bring another perspective into the discussion.

When migrating from previous JUnit versions one important factor is to not alter the tests in any way that changes the code they cover as well as the outcome. Ideally you should not need to add workarounds or any the likes either. In this case we have a change in behavior, a new / migrated feature that makes a test suddenly fail. Whether or not the failure is justified and indicates a problem in the code is not always clear, so far I have yet to encounter such a case. Most of times it's timing problems, race-conditions, slow file systems on CI, and yeah, Windows mostly 😄 That's a perspective from my experience migrating thousands of tests.

My personal perception is, that the feature @TempDir we are talking about is considered a utility by me. It should make my life easier, free me from boilerplate code and just do it's thing. If however one of these is not the case, and there is no way to make it do what it should, I will not use it. In this case it's creation and deletion of a temp file. Nothing more, nothing less. Adding that in @BeforeEach and @AfterEach myself is my workaround. Thanks to Java NIO these are usually one-liners. Plus I have 100% control over how errors should be handled or if I want to skip deletion in some cases. Working with different CleanupModes would be an option, however none of the existing options would give me what I want to achieve.

Further, if it mattered for a test if it causes open file handles or similar, these kind of checks would be included in the test. It is not the responsibility or even expectation of this utility to uncover these issues. Yet it does, which can be helpful in some cases, but there are exceptions.

With that in mind I wonder what value @TempDir brings to the table. If a feature does not add value, it becomes a burden that should either be improved or removed. Giving back some control over this feature to the user would make it more useful, wouldn't it?

strangelookingnerd avatar May 24 '25 19:05 strangelookingnerd

Adding some simplified example of the instances where I encountered issues with @TempDir.

Usually some background task using the temp directory created by the test is started before the test and stopped afterwards. Stopping the task gracefully does take some time due to slow file systems or similar.

The following snippet will fail because the @TempDir can not be deleted (yet). Touching the shutdown routine of my background task is oftentimes not desired.

class TempDirReproducerTest {

  @TempDir
  private Path tempDir;
  private FileBlockerThread thread;

  @BeforeEach
  void setUp() {
    // run something that uses the tempDir in the background
    thread = new FileBlockerThread(tempDir);
    thread.start();
  }

  @Test
  @EnabledOnOs(OS.WINDOWS)
  void testSomething() {
    // test that something is happening in the background
    assertTrue(thread.isAlive());
    assertFalse(tempDir.toFile().delete());
  }

  @AfterEach
  void tearDown() {
    // stop using the tempDir
    while (thread.isAlive()) {
      thread.interrupt();
    }
  }

  static class FileBlockerThread extends Thread {

    private final Path tempDir;

    public FileBlockerThread(Path tempDir) {
      this.tempDir = tempDir;
    }

    @Override
    public void run() {
      try {
        // create a file in temp dir
        Path blockedFile = Files.createTempFile(tempDir, "test", ".tmp");

        // prevents the file from being deleted
        RandomAccessFile raf = new RandomAccessFile(blockedFile.toFile(), "rw");
        FileChannel channel = raf.getChannel();
        FileLock lock = channel.lock();

        try {
          // simulate continuous writing to the file or similar
          Thread.sleep(1000);
        } catch (InterruptedException ie) {
          try {
            // simulate cleanup which takes a while
            Thread.sleep(1000);

            // free the lock
            lock.close();
            channel.close();
            raf.close();
          } catch (Exception ex) {
            // NOP
          }
        }
      } catch (Exception ex) {
        throw new RuntimeException(ex);
      }
    }
  }
}

strangelookingnerd avatar Jun 24 '25 14:06 strangelookingnerd

Have you considered using an ExecutorService for your background tasks and waiting for it to shut down, e.g. in an @AfterEach method? Here's an example of an Extension that does so (you might need to adjust the 5s timeout it uses): https://github.com/marcphilipp/jcon-junit-workshop-code/blob/ce0bc7b322efea78b91f66a8c038467543af5820/src/test/java/ExecutorServiceExtension.java

marcphilipp avatar Jun 26 '25 06:06 marcphilipp

Unfortunately it's a little more complicated. Imagine the background task is a complex application, a Jenkins instance used for testing plugins, build jobs and the likes. It has concurrent tasks that lock / unlock resources during runtime. The test itself does not have full control over these resources other than providing the temporary directory where they are created in. Some resources may only get unlocked once the JVM exits, causing the issue described above.

With JUnit4 this used to be a non-issue as errors during cleanup of TemporaryFolder were ignored by default. If I migrate these tests to JUnit5 and use TempDir instead, some may randomly start failing. This fact alone rightfully leads to the change / migration not being accepted by maintainers. If I now add workarounds to make the test succeed, the acceptance of changes by a maintainer depends on an explanation why the successor testing framework needs a larger amount of code / work or requires non-trivial changes in order to provide the same functionality as before.

In my opinion this is unfortunate and an option that would allow me to simply ignore these errors would be way easier. Since TempDir(cleanup = NEVER) already exists, I would not know why something like cleanup = IGNORE_ERRORS would suddenly be an issue as it would still try to cleanup, report errors in the log but not cause test failures for reasons that are entirely unrelated to the tests.

strangelookingnerd avatar Jun 26 '25 06:06 strangelookingnerd

Would another option be allowing the deferral of destroying temp resources until the end of the suite or at JVM shutdown? That'd also be useful when performing diagnostics between tests as you could compare temporary directories between test cases.

Another option I could think of would be allowing the use of a flag to change the location of the temporary directory from $TEMPDIR to target/junit. This'd be beneficial for tests making a large amount of data as a part of those test cases, since some operating systems such as Amazon Linux (2 or 2023, I forget which) still use a ramdisk for their tempfs rather than persistent storage by default. This means they are very limited in how much data you can make in a tempfs without exhausting system resources.

That aside, I do feel that when tests run, how those tests behave should have as little effect on the runner and other tests as possible. Right now, having failures due to runners and test cases trampling over file management feels like something that could be avoidable or logged as a warning.

ascopes avatar Jun 26 '25 06:06 ascopes

Would another option be allowing the deferral of destroying temp resources until the end of the suite or at JVM shutdown? That'd also be useful when performing diagnostics between tests as you could compare temporary directories between test cases.

Adding File#deleteOnExit() as a last remedy during cleanup is a good idea. However I'd have mixed feelings for something like @TempDir(cleanup = ON_EXIT) as this should be default behavior IMHO.

Another option I could think of would be allowing the use of a flag to change the location of the temporary directory from $TEMPDIR to target/junit. This'd be beneficial for tests making a large amount of data as a part of those test cases, since some operating systems such as Amazon Linux (2 or 2023, I forget which) still use a ramdisk for their tempfs rather than persistent storage. This means they are very limited in how much data you can make in a tempfs without exhausting system resources.

I don't think this would actually help my case, however I think it can already be achieved by using @TempDir(factory = TempDirFactory.class) with a custom TempDirFactory implementation.

That aside, I do feel that when tests run, how those tests behave should have as little effect on the runner and other tests as possible. Right now, having failures due to runners and test cases trampling over file management feels like something that could be avoidable or logged as a warning.

100% agree on that.

strangelookingnerd avatar Jun 26 '25 07:06 strangelookingnerd

Would another option be allowing the deferral of destroying temp resources until the end of the suite or at JVM shutdown? That'd also be useful when performing diagnostics between tests as you could compare temporary directories between test cases.

We don't control when the JVM is being shut down. For example, a tool could reuse a JVM for multiple executions. The NamespacedHierarchicalStore in LauncherSession and ExecutionRequest can already be used to attach AutoCloseable resources to the end of the session or current execution, respectively. Thus, it's possible to write a custom extension with such cleanup behavior.

Another option I could think of would be allowing the use of a flag to change the location of the temporary directory from $TEMPDIR to target/junit. This'd be beneficial for tests making a large amount of data as a part of those test cases, since some operating systems such as Amazon Linux (2 or 2023, I forget which) still use a ramdisk for their tempfs rather than persistent storage by default. This means they are very limited in how much data you can make in a tempfs without exhausting system resources.

You can already do that by specifying -Djava.io.tmpdir=target/junit when launching the test JVM.

marcphilipp avatar Jun 26 '25 07:06 marcphilipp

This behavior also bug us at Eclipse Platform. Especially on windows the file-system is sometimes "lazy" on its own, also as its good to cleanup as much as possible failing the test completely seems wrong.

A good solution seem to just try to wipe it again during a shutdown hook so at least when the process exit there is one last chance and if that did not work lets be it!

laeubi avatar Oct 01 '25 17:10 laeubi