All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.junitpioneer.jupiter.TempDirectoryExtension Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * Copyright 2015-2020 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v2.0 which
 * accompanies this distribution and is available at
 *
 * http://www.eclipse.org/legal/epl-v20.html
 */

package org.junitpioneer.jupiter;

import static java.nio.file.FileVisitResult.CONTINUE;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

/**
 * {@code TempDirectory} is a JUnit Jupiter extension to create and clean up a
 * temporary directory.
 *
 * 

The temporary directory is only created if a test or lifecycle method or * test class constructor has a parameter annotated with * {@link TempDir @TempDir}. If the parameter type is not {@link Path} or if the * temporary directory could not be created, this extension will throw a * {@link ParameterResolutionException}.

* *

The scope of the temporary directory depends on where the first * {@link TempDir @TempDir} annotation is encountered when executing a test * class. The temporary directory will be shared by all tests in a class when * the annotation is present on a parameter of a * {@link org.junit.jupiter.api.BeforeAll @BeforeAll} method or the test class * constructor. Otherwise, e.g. when only used on test or * {@link org.junit.jupiter.api.BeforeEach @BeforeEach} or * {@link org.junit.jupiter.api.AfterEach @AfterEach} methods, each test will * use its own temporary directory.

* *

When the end of the scope of a temporary directory is reached, i.e. when * the test method or class has finished execution, this extension will attempt * to recursively delete all files and directories in the temporary directory * and, finally, the temporary directory itself. In case deletion of a file or * directory fails, this extension will throw an {@link IOException} that will * cause the test to fail.

* *

By default, this extension will use the default * {@link java.nio.file.FileSystem FileSystem} to create temporary directories * in the default location. However, you may instantiate this extension using * the {@link TempDirectoryExtension#createInCustomDirectory(ParentDirProvider)} * or {@link TempDirectoryExtension#createInCustomDirectory(Callable)}} factory methods * and register it via {@link org.junit.jupiter.api.extension.RegisterExtension @RegisterExtension} * to pass a custom provider to configure the parent directory for all temporary * directories created by this extension. This allows the use of this extension * with any third-party {@code FileSystem} implementation, e.g. * Jimfs.

* *

Since JUnit Jupiter 5.4, there's a * * built-in {@code @TempDir} extension. If you don't need support for * arbitrary file systems, you should consider using that instead of this * extension.

* *

For more details and examples, see * the documentation on TempDirectory *

* * @since 0.1 * @see TempDir * @see ParentDirProvider * @see Files#createTempDirectory */ public class TempDirectoryExtension implements ParameterResolver { /** * {@code ParentDirProvider} can be used to configure a custom parent * directory for all temporary directories created by the * {@link TempDirectoryExtension} extension this is used with. * * @see org.junit.jupiter.api.extension.RegisterExtension * @see TempDirectoryExtension#createInCustomDirectory(ParentDirProvider) */ @FunctionalInterface public interface ParentDirProvider { /** * Get the parent directory for all temporary directories created by the * {@link TempDirectoryExtension} extension this is used with. * * @return the parent directory for all temporary directories */ // excluded from Sonar as java.util.concurrent.Callable is root of this generic exception Path get(ParameterContext parameterContext, ExtensionContext extensionContext) throws Exception; //NOSONAR } /** * {@code TempDirProvider} is used internally to define how the temporary * directory is created. * *

The temporary directory is by default created on the regular * file system, but the user can also provide a custom file system * by using the {@link ParentDirProvider}. An instance of * {@code TempDirProvider} executes these (and possibly other) strategies.

* * @see TempDirectoryExtension.ParentDirProvider */ @FunctionalInterface private interface TempDirProvider { CloseablePath get(ParameterContext parameterContext, ExtensionContext extensionContext, String dirPrefix); } private static final Namespace NAMESPACE = Namespace.create(TempDirectoryExtension.class); private static final String KEY = "temp.dir"; private static final String TEMP_DIR_PREFIX = "junit"; private final TempDirProvider tempDirProvider; private TempDirectoryExtension(TempDirProvider tempDirProvider) { this.tempDirProvider = requireNonNull(tempDirProvider); } /** * Create a new {@code TempDirectory} extension that uses the default * {@link java.nio.file.FileSystem FileSystem} and creates temporary * directories in the default location. * *

This constructor is used by the JUnit Jupiter Engine when the * extension is registered via * {@link org.junit.jupiter.api.extension.ExtendWith @ExtendWith}.

*/ public TempDirectoryExtension() { this((__, ___, dirPrefix) -> createDefaultTempDir(dirPrefix)); } /** * Returns a {@code TempDirectory} extension that uses the default * {@link java.nio.file.FileSystem FileSystem} and creates temporary * directories in the default location. * *

You may use this factory method when registering this extension via * {@link org.junit.jupiter.api.extension.RegisterExtension @RegisterExtension}, * although you might prefer the simpler registration via * {@link org.junit.jupiter.api.extension.ExtendWith @ExtendWith}.

* * @return a {@code TempDirectory} extension */ public static TempDirectoryExtension createInDefaultDirectory() { return new TempDirectoryExtension(); } /** * Returns a {@code TempDirectory} extension that uses the supplied * {@link ParentDirProvider} to configure the parent directory for the * temporary directories created by this extension. * *

You may use this factory method when registering this extension via * {@link org.junit.jupiter.api.extension.RegisterExtension @RegisterExtension}.

* * @param parentDirProvider used to configure the parent directory for the * temporary directories created by this extension */ public static TempDirectoryExtension createInCustomDirectory(ParentDirProvider parentDirProvider) { requireNonNull(parentDirProvider); return new TempDirectoryExtension((parameterContext, extensionContext, dirPrefix) -> createCustomTempDir(parentDirProvider, parameterContext, extensionContext, dirPrefix)); } /** * Returns a {@code TempDirectory} extension that uses the supplied * {@link Callable} to configure the parent directory for the temporary * directories created by this extension. * *

You may use this factory method when registering this extension via * {@link org.junit.jupiter.api.extension.RegisterExtension @RegisterExtension}.

* * @param parentDirProvider used to configure the parent directory for the * temporary directories created by this extension */ public static TempDirectoryExtension createInCustomDirectory(Callable parentDirProvider) { requireNonNull(parentDirProvider); return createInCustomDirectory((parameterContext, extensionContext) -> parentDirProvider.call()); } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.isAnnotated(TempDir.class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Class parameterType = parameterContext.getParameter().getType(); if (parameterType != Path.class) { throw new ParameterResolutionException( "Can only resolve parameter of type " + Path.class.getName() + " but was: " + parameterType.getName()); } return extensionContext .getStore(NAMESPACE) // .getOrComputeIfAbsent(KEY, key -> tempDirProvider.get(parameterContext, extensionContext, TEMP_DIR_PREFIX), CloseablePath.class) // .get(); } private static CloseablePath createDefaultTempDir(String dirPrefix) { try { return new CloseablePath(Files.createTempDirectory(dirPrefix)); } catch (Exception ex) { throw new ExtensionConfigurationException("Failed to create default temp directory", ex); } } private static CloseablePath createCustomTempDir(ParentDirProvider parentDirProvider, ParameterContext parameterContext, ExtensionContext extensionContext, String dirPrefix) { Path parentDir; try { parentDir = parentDirProvider.get(parameterContext, extensionContext); requireNonNull(parentDir); } catch (Exception ex) { throw new ParameterResolutionException("Failed to get parent directory from provider", ex); } try { return new CloseablePath(Files.createTempDirectory(parentDir, dirPrefix)); } catch (Exception ex) { throw new ParameterResolutionException("Failed to create custom temp directory", ex); } } private static class CloseablePath implements CloseableResource { private final Path dir; CloseablePath(Path dir) { this.dir = dir; } Path get() { return dir; } @Override public void close() throws IOException { SortedMap failures = deleteAllFilesAndDirectories(); if (!failures.isEmpty()) { throw createIOExceptionWithAttachedFailures(failures); } } private SortedMap deleteAllFilesAndDirectories() throws IOException { SortedMap failures = new TreeMap<>(); Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { return deleteAndContinue(file); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { if (exc instanceof NoSuchFileException) { return CONTINUE; } return super.visitFileFailed(file, exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) { return deleteAndContinue(dir); } private FileVisitResult deleteAndContinue(Path path) { try { // without races by multiple threads, a call to `Files::delete` would suffice // because the tree walker doesn't visit non-existing files; since race // conditions can't be ruled out, `Files::deleteIfExists` is the safer approach Files.deleteIfExists(path); } catch (IOException ex) { failures.put(path, ex); } return CONTINUE; } }); return failures; } private IOException createIOExceptionWithAttachedFailures(SortedMap failures) { String joinedPaths = failures .keySet() .stream() .peek(this::tryToDeleteOnExit) .map(this::relativizeSafely) .map(String::valueOf) .collect(joining(", ")); IOException exception = new IOException("Failed to delete temp directory " + dir.toAbsolutePath() + ". The following paths could not be deleted (see suppressed exceptions for details): " + joinedPaths); failures.values().forEach(exception::addSuppressed); return exception; } private void tryToDeleteOnExit(Path path) { try { path.toFile().deleteOnExit(); } catch (UnsupportedOperationException ignore) { // If the `Path` can't be turned into a `File` (which throws the UOE), // it can't be registered to be deleted when the JVM terminates. // Because deleting on JVM termination is just a last ditch effort and // nicety towards the user, it is entirely optional and shouldn't affect // the extension's behavior. } } private Path relativizeSafely(Path path) { try { return dir.relativize(path); } catch (IllegalArgumentException e) { return path; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy