1 package org.junit.rules; 2 3 import static org.junit.Assert.fail; 4 5 import java.io.File; 6 import java.io.IOException; 7 import java.lang.reflect.Array; 8 import java.lang.reflect.InvocationTargetException; 9 import java.lang.reflect.Method; 10 11 import org.junit.Rule; 12 13 /** 14 * The TemporaryFolder Rule allows creation of files and folders that should 15 * be deleted when the test method finishes (whether it passes or 16 * fails). 17 * By default no exception will be thrown in case the deletion fails. 18 * 19 * <p>Example of usage: 20 * <pre> 21 * public static class HasTempFolder { 22 * @Rule 23 * public TemporaryFolder folder= new TemporaryFolder(); 24 * 25 * @Test 26 * public void testUsingTempFolder() throws IOException { 27 * File createdFile= folder.newFile("myfile.txt"); 28 * File createdFolder= folder.newFolder("subfolder"); 29 * // ... 30 * } 31 * } 32 * </pre> 33 * 34 * <p>TemporaryFolder rule supports assured deletion mode, which 35 * will fail the test in case deletion fails with {@link AssertionError}. 36 * 37 * <p>Creating TemporaryFolder with assured deletion: 38 * <pre> 39 * @Rule 40 * public TemporaryFolder folder= TemporaryFolder.builder().assureDeletion().build(); 41 * </pre> 42 * 43 * @since 4.7 44 */ 45 public class TemporaryFolder extends ExternalResource { 46 private final File parentFolder; 47 private final boolean assureDeletion; 48 private File folder; 49 50 private static final int TEMP_DIR_ATTEMPTS = 10000; 51 private static final String TMP_PREFIX = "junit"; 52 53 /** 54 * Create a temporary folder which uses system default temporary-file 55 * directory to create temporary resources. 56 */ TemporaryFolder()57 public TemporaryFolder() { 58 this((File) null); 59 } 60 61 /** 62 * Create a temporary folder which uses the specified directory to create 63 * temporary resources. 64 * 65 * @param parentFolder folder where temporary resources will be created. 66 * If {@code null} then system default temporary-file directory is used. 67 */ TemporaryFolder(File parentFolder)68 public TemporaryFolder(File parentFolder) { 69 this.parentFolder = parentFolder; 70 this.assureDeletion = false; 71 } 72 73 /** 74 * Create a {@link TemporaryFolder} initialized with 75 * values from a builder. 76 */ TemporaryFolder(Builder builder)77 protected TemporaryFolder(Builder builder) { 78 this.parentFolder = builder.parentFolder; 79 this.assureDeletion = builder.assureDeletion; 80 } 81 82 /** 83 * Returns a new builder for building an instance of {@link TemporaryFolder}. 84 * 85 * @since 4.13 86 */ builder()87 public static Builder builder() { 88 return new Builder(); 89 } 90 91 /** 92 * Builds an instance of {@link TemporaryFolder}. 93 * 94 * @since 4.13 95 */ 96 public static class Builder { 97 private File parentFolder; 98 private boolean assureDeletion; 99 Builder()100 protected Builder() {} 101 102 /** 103 * Specifies which folder to use for creating temporary resources. 104 * If {@code null} then system default temporary-file directory is 105 * used. 106 * 107 * @return this 108 */ parentFolder(File parentFolder)109 public Builder parentFolder(File parentFolder) { 110 this.parentFolder = parentFolder; 111 return this; 112 } 113 114 /** 115 * Setting this flag assures that no resources are left undeleted. Failure 116 * to fulfill the assurance results in failure of tests with an 117 * {@link AssertionError}. 118 * 119 * @return this 120 */ assureDeletion()121 public Builder assureDeletion() { 122 this.assureDeletion = true; 123 return this; 124 } 125 126 /** 127 * Builds a {@link TemporaryFolder} instance using the values in this builder. 128 */ build()129 public TemporaryFolder build() { 130 return new TemporaryFolder(this); 131 } 132 } 133 134 @Override before()135 protected void before() throws Throwable { 136 create(); 137 } 138 139 @Override after()140 protected void after() { 141 delete(); 142 } 143 144 // testing purposes only 145 146 /** 147 * for testing purposes only. Do not use. 148 */ create()149 public void create() throws IOException { 150 folder = createTemporaryFolderIn(parentFolder); 151 } 152 153 /** 154 * Returns a new fresh file with the given name under the temporary folder. 155 */ newFile(String fileName)156 public File newFile(String fileName) throws IOException { 157 File file = new File(getRoot(), fileName); 158 if (!file.createNewFile()) { 159 throw new IOException( 160 "a file with the name \'" + fileName + "\' already exists in the test folder"); 161 } 162 return file; 163 } 164 165 /** 166 * Returns a new fresh file with a random name under the temporary folder. 167 */ newFile()168 public File newFile() throws IOException { 169 return File.createTempFile(TMP_PREFIX, null, getRoot()); 170 } 171 172 /** 173 * Returns a new fresh folder with the given path under the temporary 174 * folder. 175 */ newFolder(String path)176 public File newFolder(String path) throws IOException { 177 return newFolder(new String[]{path}); 178 } 179 180 /** 181 * Returns a new fresh folder with the given paths under the temporary 182 * folder. For example, if you pass in the strings {@code "parent"} and {@code "child"} 183 * then a directory named {@code "parent"} will be created under the temporary folder 184 * and a directory named {@code "child"} will be created under the newly-created 185 * {@code "parent"} directory. 186 */ newFolder(String... paths)187 public File newFolder(String... paths) throws IOException { 188 if (paths.length == 0) { 189 throw new IllegalArgumentException("must pass at least one path"); 190 } 191 192 /* 193 * Before checking if the paths are absolute paths, check if create() was ever called, 194 * and if it wasn't, throw IllegalStateException. 195 */ 196 File root = getRoot(); 197 for (String path : paths) { 198 if (new File(path).isAbsolute()) { 199 throw new IOException("folder path \'" + path + "\' is not a relative path"); 200 } 201 } 202 203 File relativePath = null; 204 File file = root; 205 boolean lastMkdirsCallSuccessful = true; 206 for (String path : paths) { 207 relativePath = new File(relativePath, path); 208 file = new File(root, relativePath.getPath()); 209 210 lastMkdirsCallSuccessful = file.mkdirs(); 211 if (!lastMkdirsCallSuccessful && !file.isDirectory()) { 212 if (file.exists()) { 213 throw new IOException( 214 "a file with the path \'" + relativePath.getPath() + "\' exists"); 215 } else { 216 throw new IOException( 217 "could not create a folder with the path \'" + relativePath.getPath() + "\'"); 218 } 219 } 220 } 221 if (!lastMkdirsCallSuccessful) { 222 throw new IOException( 223 "a folder with the path \'" + relativePath.getPath() + "\' already exists"); 224 } 225 return file; 226 } 227 228 /** 229 * Returns a new fresh folder with a random name under the temporary folder. 230 */ newFolder()231 public File newFolder() throws IOException { 232 return createTemporaryFolderIn(getRoot()); 233 } 234 createTemporaryFolderIn(File parentFolder)235 private static File createTemporaryFolderIn(File parentFolder) throws IOException { 236 try { 237 return createTemporaryFolderWithNioApi(parentFolder); 238 } catch (ClassNotFoundException ignore) { 239 // Fallback for Java 5 and 6 240 return createTemporaryFolderWithFileApi(parentFolder); 241 } catch (InvocationTargetException e) { 242 Throwable cause = e.getCause(); 243 if (cause instanceof IOException) { 244 throw (IOException) cause; 245 } 246 if (cause instanceof RuntimeException) { 247 throw (RuntimeException) cause; 248 } 249 IOException exception = new IOException("Failed to create temporary folder in " + parentFolder); 250 exception.initCause(cause); 251 throw exception; 252 } catch (Exception e) { 253 throw new RuntimeException("Failed to create temporary folder in " + parentFolder, e); 254 } 255 } 256 createTemporaryFolderWithNioApi(File parentFolder)257 private static File createTemporaryFolderWithNioApi(File parentFolder) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 258 Class<?> filesClass = Class.forName("java.nio.file.Files"); 259 Object fileAttributeArray = Array.newInstance(Class.forName("java.nio.file.attribute.FileAttribute"), 0); 260 Class<?> pathClass = Class.forName("java.nio.file.Path"); 261 Object tempDir; 262 if (parentFolder != null) { 263 Method createTempDirectoryMethod = filesClass.getDeclaredMethod("createTempDirectory", pathClass, String.class, fileAttributeArray.getClass()); 264 Object parentPath = File.class.getDeclaredMethod("toPath").invoke(parentFolder); 265 tempDir = createTempDirectoryMethod.invoke(null, parentPath, TMP_PREFIX, fileAttributeArray); 266 } else { 267 Method createTempDirectoryMethod = filesClass.getDeclaredMethod("createTempDirectory", String.class, fileAttributeArray.getClass()); 268 tempDir = createTempDirectoryMethod.invoke(null, TMP_PREFIX, fileAttributeArray); 269 } 270 return (File) pathClass.getDeclaredMethod("toFile").invoke(tempDir); 271 } 272 createTemporaryFolderWithFileApi(File parentFolder)273 private static File createTemporaryFolderWithFileApi(File parentFolder) throws IOException { 274 File createdFolder = null; 275 for (int i = 0; i < TEMP_DIR_ATTEMPTS; ++i) { 276 // Use createTempFile to get a suitable folder name. 277 String suffix = ".tmp"; 278 File tmpFile = File.createTempFile(TMP_PREFIX, suffix, parentFolder); 279 String tmpName = tmpFile.toString(); 280 // Discard .tmp suffix of tmpName. 281 String folderName = tmpName.substring(0, tmpName.length() - suffix.length()); 282 createdFolder = new File(folderName); 283 if (createdFolder.mkdir()) { 284 tmpFile.delete(); 285 return createdFolder; 286 } 287 tmpFile.delete(); 288 } 289 throw new IOException("Unable to create temporary directory in: " 290 + parentFolder.toString() + ". Tried " + TEMP_DIR_ATTEMPTS + " times. " 291 + "Last attempted to create: " + createdFolder.toString()); 292 } 293 294 /** 295 * @return the location of this temporary folder. 296 */ getRoot()297 public File getRoot() { 298 if (folder == null) { 299 throw new IllegalStateException( 300 "the temporary folder has not yet been created"); 301 } 302 return folder; 303 } 304 305 /** 306 * Delete all files and folders under the temporary folder. Usually not 307 * called directly, since it is automatically applied by the {@link Rule}. 308 * 309 * @throws AssertionError if unable to clean up resources 310 * and deletion of resources is assured. 311 */ delete()312 public void delete() { 313 if (!tryDelete()) { 314 if (assureDeletion) { 315 fail("Unable to clean up temporary folder " + folder); 316 } 317 } 318 } 319 320 /** 321 * Tries to delete all files and folders under the temporary folder and 322 * returns whether deletion was successful or not. 323 * 324 * @return {@code true} if all resources are deleted successfully, 325 * {@code false} otherwise. 326 */ tryDelete()327 private boolean tryDelete() { 328 if (folder == null) { 329 return true; 330 } 331 332 return recursiveDelete(folder); 333 } 334 recursiveDelete(File file)335 private boolean recursiveDelete(File file) { 336 // Try deleting file before assuming file is a directory 337 // to prevent following symbolic links. 338 if (file.delete()) { 339 return true; 340 } 341 File[] files = file.listFiles(); 342 if (files != null) { 343 for (File each : files) { 344 if (!recursiveDelete(each)) { 345 return false; 346 } 347 } 348 } 349 return file.delete(); 350 } 351 } 352