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
008 import org.junit.Rule;
009
010 /**
011 * The TemporaryFolder Rule allows creation of files and folders that should
012 * be deleted when the test method finishes (whether it passes or
013 * fails).
014 * By default no exception will be thrown in case the deletion fails.
015 *
016 * <p>Example of usage:
017 * <pre>
018 * public static class HasTempFolder {
019 * @Rule
020 * public TemporaryFolder folder= new TemporaryFolder();
021 *
022 * @Test
023 * public void testUsingTempFolder() throws IOException {
024 * File createdFile= folder.newFile("myfile.txt");
025 * File createdFolder= folder.newFolder("subfolder");
026 * // ...
027 * }
028 * }
029 * </pre>
030 *
031 * <p>TemporaryFolder rule supports assured deletion mode, which
032 * will fail the test in case deletion fails with {@link AssertionError}.
033 *
034 * <p>Creating TemporaryFolder with assured deletion:
035 * <pre>
036 * @Rule
037 * public TemporaryFolder folder= TemporaryFolder.builder().assureDeletion().build();
038 * </pre>
039 *
040 * @since 4.7
041 */
042 public class TemporaryFolder extends ExternalResource {
043 private final File parentFolder;
044 private final boolean assureDeletion;
045 private File folder;
046
047 private static final int TEMP_DIR_ATTEMPTS = 10000;
048 private static final String TMP_PREFIX = "junit";
049
050 /**
051 * Create a temporary folder which uses system default temporary-file
052 * directory to create temporary resources.
053 */
054 public TemporaryFolder() {
055 this((File) null);
056 }
057
058 /**
059 * Create a temporary folder which uses the specified directory to create
060 * temporary resources.
061 *
062 * @param parentFolder folder where temporary resources will be created.
063 * If {@code null} then system default temporary-file directory is used.
064 */
065 public TemporaryFolder(File parentFolder) {
066 this.parentFolder = parentFolder;
067 this.assureDeletion = false;
068 }
069
070 /**
071 * Create a {@link TemporaryFolder} initialized with
072 * values from a builder.
073 */
074 protected TemporaryFolder(Builder builder) {
075 this.parentFolder = builder.parentFolder;
076 this.assureDeletion = builder.assureDeletion;
077 }
078
079 /**
080 * Returns a new builder for building an instance of {@link TemporaryFolder}.
081 *
082 * @since 4.13
083 */
084 public static Builder builder() {
085 return new Builder();
086 }
087
088 /**
089 * Builds an instance of {@link TemporaryFolder}.
090 *
091 * @since 4.13
092 */
093 public static class Builder {
094 private File parentFolder;
095 private boolean assureDeletion;
096
097 protected Builder() {}
098
099 /**
100 * Specifies which folder to use for creating temporary resources.
101 * If {@code null} then system default temporary-file directory is
102 * used.
103 *
104 * @return this
105 */
106 public Builder parentFolder(File parentFolder) {
107 this.parentFolder = parentFolder;
108 return this;
109 }
110
111 /**
112 * Setting this flag assures that no resources are left undeleted. Failure
113 * to fulfill the assurance results in failure of tests with an
114 * {@link AssertionError}.
115 *
116 * @return this
117 */
118 public Builder assureDeletion() {
119 this.assureDeletion = true;
120 return this;
121 }
122
123 /**
124 * Builds a {@link TemporaryFolder} instance using the values in this builder.
125 */
126 public TemporaryFolder build() {
127 return new TemporaryFolder(this);
128 }
129 }
130
131 @Override
132 protected void before() throws Throwable {
133 create();
134 }
135
136 @Override
137 protected void after() {
138 delete();
139 }
140
141 // testing purposes only
142
143 /**
144 * for testing purposes only. Do not use.
145 */
146 public void create() throws IOException {
147 folder = createTemporaryFolderIn(parentFolder);
148 }
149
150 /**
151 * Returns a new fresh file with the given name under the temporary folder.
152 */
153 public File newFile(String fileName) throws IOException {
154 File file = new File(getRoot(), fileName);
155 if (!file.createNewFile()) {
156 throw new IOException(
157 "a file with the name \'" + fileName + "\' already exists in the test folder");
158 }
159 return file;
160 }
161
162 /**
163 * Returns a new fresh file with a random name under the temporary folder.
164 */
165 public File newFile() throws IOException {
166 return File.createTempFile(TMP_PREFIX, null, getRoot());
167 }
168
169 /**
170 * Returns a new fresh folder with the given path under the temporary
171 * folder.
172 */
173 public File newFolder(String path) throws IOException {
174 return newFolder(new String[]{path});
175 }
176
177 /**
178 * Returns a new fresh folder with the given paths under the temporary
179 * folder. For example, if you pass in the strings {@code "parent"} and {@code "child"}
180 * then a directory named {@code "parent"} will be created under the temporary folder
181 * and a directory named {@code "child"} will be created under the newly-created
182 * {@code "parent"} directory.
183 */
184 public File newFolder(String... paths) throws IOException {
185 if (paths.length == 0) {
186 throw new IllegalArgumentException("must pass at least one path");
187 }
188
189 /*
190 * Before checking if the paths are absolute paths, check if create() was ever called,
191 * and if it wasn't, throw IllegalStateException.
192 */
193 File root = getRoot();
194 for (String path : paths) {
195 if (new File(path).isAbsolute()) {
196 throw new IOException("folder path \'" + path + "\' is not a relative path");
197 }
198 }
199
200 File relativePath = null;
201 File file = root;
202 boolean lastMkdirsCallSuccessful = true;
203 for (String path : paths) {
204 relativePath = new File(relativePath, path);
205 file = new File(root, relativePath.getPath());
206
207 lastMkdirsCallSuccessful = file.mkdirs();
208 if (!lastMkdirsCallSuccessful && !file.isDirectory()) {
209 if (file.exists()) {
210 throw new IOException(
211 "a file with the path \'" + relativePath.getPath() + "\' exists");
212 } else {
213 throw new IOException(
214 "could not create a folder with the path \'" + relativePath.getPath() + "\'");
215 }
216 }
217 }
218 if (!lastMkdirsCallSuccessful) {
219 throw new IOException(
220 "a folder with the path \'" + relativePath.getPath() + "\' already exists");
221 }
222 return file;
223 }
224
225 /**
226 * Returns a new fresh folder with a random name under the temporary folder.
227 */
228 public File newFolder() throws IOException {
229 return createTemporaryFolderIn(getRoot());
230 }
231
232 private File createTemporaryFolderIn(File parentFolder) throws IOException {
233 File createdFolder = null;
234 for (int i = 0; i < TEMP_DIR_ATTEMPTS; ++i) {
235 // Use createTempFile to get a suitable folder name.
236 String suffix = ".tmp";
237 File tmpFile = File.createTempFile(TMP_PREFIX, suffix, parentFolder);
238 String tmpName = tmpFile.toString();
239 // Discard .tmp suffix of tmpName.
240 String folderName = tmpName.substring(0, tmpName.length() - suffix.length());
241 createdFolder = new File(folderName);
242 if (createdFolder.mkdir()) {
243 tmpFile.delete();
244 return createdFolder;
245 }
246 tmpFile.delete();
247 }
248 throw new IOException("Unable to create temporary directory in: "
249 + parentFolder.toString() + ". Tried " + TEMP_DIR_ATTEMPTS + " times. "
250 + "Last attempted to create: " + createdFolder.toString());
251 }
252
253 /**
254 * @return the location of this temporary folder.
255 */
256 public File getRoot() {
257 if (folder == null) {
258 throw new IllegalStateException(
259 "the temporary folder has not yet been created");
260 }
261 return folder;
262 }
263
264 /**
265 * Delete all files and folders under the temporary folder. Usually not
266 * called directly, since it is automatically applied by the {@link Rule}.
267 *
268 * @throws AssertionError if unable to clean up resources
269 * and deletion of resources is assured.
270 */
271 public void delete() {
272 if (!tryDelete()) {
273 if (assureDeletion) {
274 fail("Unable to clean up temporary folder " + folder);
275 }
276 }
277 }
278
279 /**
280 * Tries to delete all files and folders under the temporary folder and
281 * returns whether deletion was successful or not.
282 *
283 * @return {@code true} if all resources are deleted successfully,
284 * {@code false} otherwise.
285 */
286 private boolean tryDelete() {
287 if (folder == null) {
288 return true;
289 }
290
291 return recursiveDelete(folder);
292 }
293
294 private boolean recursiveDelete(File file) {
295 // Try deleting file before assuming file is a directory
296 // to prevent following symbolic links.
297 if (file.delete()) {
298 return true;
299 }
300 File[] files = file.listFiles();
301 if (files != null) {
302 for (File each : files) {
303 if (!recursiveDelete(each)) {
304 return false;
305 }
306 }
307 }
308 return file.delete();
309 }
310 }