001 package org.junit.rules;
002
003 import static org.junit.Assert.fail;
004
005 import java.io.File;
006 import java.io.IOException;
007 import java.lang.reflect.Array;
008 import java.lang.reflect.InvocationTargetException;
009 import java.lang.reflect.Method;
010
011 import org.junit.Rule;
012
013 /**
014 * The TemporaryFolder Rule allows creation of files and folders that should
015 * be deleted when the test method finishes (whether it passes or
016 * fails).
017 * By default no exception will be thrown in case the deletion fails.
018 *
019 * <p>Example of usage:
020 * <pre>
021 * public static class HasTempFolder {
022 * @Rule
023 * public TemporaryFolder folder= new TemporaryFolder();
024 *
025 * @Test
026 * public void testUsingTempFolder() throws IOException {
027 * File createdFile= folder.newFile("myfile.txt");
028 * File createdFolder= folder.newFolder("subfolder");
029 * // ...
030 * }
031 * }
032 * </pre>
033 *
034 * <p>TemporaryFolder rule supports assured deletion mode, which
035 * will fail the test in case deletion fails with {@link AssertionError}.
036 *
037 * <p>Creating TemporaryFolder with assured deletion:
038 * <pre>
039 * @Rule
040 * public TemporaryFolder folder= TemporaryFolder.builder().assureDeletion().build();
041 * </pre>
042 *
043 * @since 4.7
044 */
045 public class TemporaryFolder extends ExternalResource {
046 private final File parentFolder;
047 private final boolean assureDeletion;
048 private File folder;
049
050 private static final int TEMP_DIR_ATTEMPTS = 10000;
051 private static final String TMP_PREFIX = "junit";
052
053 /**
054 * Create a temporary folder which uses system default temporary-file
055 * directory to create temporary resources.
056 */
057 public TemporaryFolder() {
058 this((File) null);
059 }
060
061 /**
062 * Create a temporary folder which uses the specified directory to create
063 * temporary resources.
064 *
065 * @param parentFolder folder where temporary resources will be created.
066 * If {@code null} then system default temporary-file directory is used.
067 */
068 public TemporaryFolder(File parentFolder) {
069 this.parentFolder = parentFolder;
070 this.assureDeletion = false;
071 }
072
073 /**
074 * Create a {@link TemporaryFolder} initialized with
075 * values from a builder.
076 */
077 protected TemporaryFolder(Builder builder) {
078 this.parentFolder = builder.parentFolder;
079 this.assureDeletion = builder.assureDeletion;
080 }
081
082 /**
083 * Returns a new builder for building an instance of {@link TemporaryFolder}.
084 *
085 * @since 4.13
086 */
087 public static Builder builder() {
088 return new Builder();
089 }
090
091 /**
092 * Builds an instance of {@link TemporaryFolder}.
093 *
094 * @since 4.13
095 */
096 public static class Builder {
097 private File parentFolder;
098 private boolean assureDeletion;
099
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 */
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 */
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 */
129 public TemporaryFolder build() {
130 return new TemporaryFolder(this);
131 }
132 }
133
134 @Override
135 protected void before() throws Throwable {
136 create();
137 }
138
139 @Override
140 protected void after() {
141 delete();
142 }
143
144 // testing purposes only
145
146 /**
147 * for testing purposes only. Do not use.
148 */
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 */
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 */
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 */
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 */
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 */
231 public File newFolder() throws IOException {
232 return createTemporaryFolderIn(getRoot());
233 }
234
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
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
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 */
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 */
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 */
327 private boolean tryDelete() {
328 if (folder == null) {
329 return true;
330 }
331
332 return recursiveDelete(folder);
333 }
334
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 }