View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNull;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.BufferedOutputStream;
26  import java.io.File;
27  import java.io.FileNotFoundException;
28  import java.io.IOException;
29  import java.io.RandomAccessFile;
30  import java.lang.ref.ReferenceQueue;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.ArrayList;
35  import java.util.List;
36  
37  import org.apache.commons.io.file.AbstractTempDirTest;
38  import org.apache.commons.io.test.TestUtils;
39  import org.junit.jupiter.api.AfterEach;
40  import org.junit.jupiter.api.BeforeEach;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * Tests {@link FileCleaningTracker}.
45   */
46  class FileCleaningTrackerTest extends AbstractTempDirTest {
47  
48      private File testFile;
49      private Path testPath;
50  
51      private FileCleaningTracker fileCleaningTracker;
52  
53      RandomAccessFile createRandomAccessFile() throws FileNotFoundException {
54          return RandomAccessFileMode.READ_WRITE.create(testFile);
55      }
56  
57      private void gcFinalize() {
58          System.gc();
59          System.runFinalization();
60      }
61  
62      protected FileCleaningTracker newInstance() {
63          return new FileCleaningTracker();
64      }
65  
66      private void pauseForDeleteToComplete(File file) {
67          int count = 0;
68          while (file.exists() && count++ < 40) {
69              TestUtils.sleepQuietly(500L);
70              file = new File(file.getPath());
71          }
72      }
73  
74      private void pauseForDeleteToComplete(Path file) {
75          int count = 0;
76          while (Files.exists(file) && count++ < 40) {
77              TestUtils.sleepQuietly(500L);
78              file = Paths.get(file.toAbsolutePath().toString());
79          }
80      }
81  
82      @BeforeEach
83      public void setUp() {
84          testFile = new File(tempDirFile, "file-test.txt");
85          testPath = testFile.toPath();
86          fileCleaningTracker = newInstance();
87      }
88  
89      private String showFailures() {
90          if (fileCleaningTracker.deleteFailures.size() == 1) {
91              return "[Delete Failed: " + fileCleaningTracker.deleteFailures.get(0) + "]";
92          }
93          return "[Delete Failures: " + fileCleaningTracker.deleteFailures.size() + "]";
94      }
95  
96      @AfterEach
97      public void tearDown() {
98          // reset file cleaner class, so as not to break other tests
99          /**
100          * The following block of code can possibly be removed when the deprecated {@link FileCleaner} is gone. The question is, whether we want to support
101          * reuse of {@link FileCleaningTracker} instances, which we should, IMO, not.
102          */
103         {
104             if (fileCleaningTracker != null) {
105                 if (fileCleaningTracker.reaper != null) {
106                     fileCleaningTracker.reaper.interrupt();
107                 }
108                 fileCleaningTracker.refQueue = new ReferenceQueue<>();
109                 fileCleaningTracker.trackers.clear();
110                 fileCleaningTracker.deleteFailures.clear();
111                 fileCleaningTracker.exitWhenFinished = false;
112                 fileCleaningTracker.reaper = null;
113             }
114         }
115         fileCleaningTracker = null;
116     }
117 
118     @Test
119     void testFileCleanerDirectory_ForceStrategy_FileSource() throws Exception {
120         if (!testFile.getParentFile().exists()) {
121             throw new IOException("Cannot create file " + testFile + " as the parent directory does not exist");
122         }
123         try (BufferedOutputStream output =
124                 new BufferedOutputStream(Files.newOutputStream(testFile.toPath()))) {
125             TestUtils.generateTestData(output, 100);
126         }
127         assertTrue(testFile.exists());
128         assertTrue(tempDirFile.exists());
129 
130         Object obj = new Object();
131         assertEquals(0, fileCleaningTracker.getTrackCount());
132         fileCleaningTracker.track(tempDirFile, obj, FileDeleteStrategy.FORCE);
133         assertEquals(1, fileCleaningTracker.getTrackCount());
134 
135         obj = null;
136 
137         waitUntilTrackCount0();
138         pauseForDeleteToComplete(testFile.getParentFile());
139 
140         assertEquals(0, fileCleaningTracker.getTrackCount());
141         assertFalse(new File(testFile.getPath()).exists(), showFailures());
142         assertFalse(testFile.getParentFile().exists(), showFailures());
143     }
144 
145     @Test
146     void testFileCleanerDirectory_ForceStrategy_PathSource() throws Exception {
147         if (!Files.exists(testPath.getParent())) {
148             throw new IOException("Cannot create file " + testPath + " as the parent directory does not exist");
149         }
150         try (BufferedOutputStream output =
151                 new BufferedOutputStream(Files.newOutputStream(testPath))) {
152             TestUtils.generateTestData(output, 100);
153         }
154         assertTrue(Files.exists(testPath));
155         assertTrue(Files.exists(tempDirPath));
156 
157         Object obj = new Object();
158         assertEquals(0, fileCleaningTracker.getTrackCount());
159         fileCleaningTracker.track(tempDirPath, obj, FileDeleteStrategy.FORCE);
160         assertEquals(1, fileCleaningTracker.getTrackCount());
161 
162         obj = null;
163 
164         waitUntilTrackCount0();
165         pauseForDeleteToComplete(testPath.getParent());
166 
167         assertEquals(0, fileCleaningTracker.getTrackCount());
168         assertFalse(Files.exists(testPath), showFailures());
169         assertFalse(Files.exists(testPath.getParent()), showFailures());
170     }
171 
172     @Test
173     void testFileCleanerDirectory_NullStrategy() throws Exception {
174         TestUtils.createFile(testFile, 100);
175         assertTrue(testFile.exists());
176         assertTrue(tempDirFile.exists());
177 
178         Object obj = new Object();
179         assertEquals(0, fileCleaningTracker.getTrackCount());
180         fileCleaningTracker.track(tempDirFile, obj, null);
181         assertEquals(1, fileCleaningTracker.getTrackCount());
182 
183         obj = null;
184 
185         waitUntilTrackCount0();
186 
187         assertEquals(0, fileCleaningTracker.getTrackCount());
188         assertTrue(testFile.exists());  // not deleted, as dir not empty
189         assertTrue(testFile.getParentFile().exists());  // not deleted, as dir not empty
190     }
191 
192     @Test
193     void testFileCleanerDirectoryFileSource() throws Exception {
194         TestUtils.createFile(testFile, 100);
195         assertTrue(testFile.exists());
196         assertTrue(tempDirFile.exists());
197 
198         Object obj = new Object();
199         assertEquals(0, fileCleaningTracker.getTrackCount());
200         fileCleaningTracker.track(tempDirFile, obj);
201         assertEquals(1, fileCleaningTracker.getTrackCount());
202 
203         obj = null;
204 
205         waitUntilTrackCount0();
206 
207         assertEquals(0, fileCleaningTracker.getTrackCount());
208         assertTrue(testFile.exists());  // not deleted, as dir not empty
209         assertTrue(testFile.getParentFile().exists());  // not deleted, as dir not empty
210     }
211 
212     @Test
213     void testFileCleanerDirectoryPathSource() throws Exception {
214         TestUtils.createFile(testPath, 100);
215         assertTrue(Files.exists(testPath));
216         assertTrue(Files.exists(tempDirPath));
217 
218         Object obj = new Object();
219         assertEquals(0, fileCleaningTracker.getTrackCount());
220         fileCleaningTracker.track(tempDirPath, obj);
221         assertEquals(1, fileCleaningTracker.getTrackCount());
222 
223         obj = null;
224 
225         waitUntilTrackCount0();
226 
227         assertEquals(0, fileCleaningTracker.getTrackCount());
228         assertTrue(Files.exists(testPath));  // not deleted, as dir not empty
229         assertTrue(Files.exists(testPath.getParent()));  // not deleted, as dir not empty
230     }
231 
232     @Test
233     void testFileCleanerExitWhenFinished_NoTrackAfter() {
234         assertFalse(fileCleaningTracker.exitWhenFinished);
235         fileCleaningTracker.exitWhenFinished();
236         assertTrue(fileCleaningTracker.exitWhenFinished);
237         assertNull(fileCleaningTracker.reaper);
238 
239         final String path = testFile.getPath();
240         final Object marker = new Object();
241 
242         assertThrows(IllegalStateException.class, () -> fileCleaningTracker.track(path, marker));
243         assertTrue(fileCleaningTracker.exitWhenFinished);
244         assertNull(fileCleaningTracker.reaper);
245     }
246 
247     @Test
248     void testFileCleanerExitWhenFinished1() throws Exception {
249         final String path = testFile.getPath();
250 
251         assertFalse(testFile.exists(), "1-testFile exists: " + testFile);
252 
253         // Do NOT used a try-with-resources statement here or the test will fail.
254         RandomAccessFile raf = createRandomAccessFile();
255         assertTrue(testFile.exists(), "2-testFile exists");
256 
257         assertEquals(0, fileCleaningTracker.getTrackCount(), "3-Track Count");
258         fileCleaningTracker.track(path, raf);
259         assertEquals(1, fileCleaningTracker.getTrackCount(), "4-Track Count");
260         assertFalse(fileCleaningTracker.exitWhenFinished, "5-exitWhenFinished");
261         assertTrue(fileCleaningTracker.reaper.isAlive(), "6-reaper.isAlive");
262 
263         assertFalse(fileCleaningTracker.exitWhenFinished, "7-exitWhenFinished");
264         fileCleaningTracker.exitWhenFinished();
265         assertTrue(fileCleaningTracker.exitWhenFinished, "8-exitWhenFinished");
266         assertTrue(fileCleaningTracker.reaper.isAlive(), "9-reaper.isAlive");
267 
268         raf.close();
269         testFile = null;
270         raf = null;
271 
272         waitUntilTrackCount0();
273         pauseForDeleteToComplete(new File(path));
274 
275         assertEquals(0, fileCleaningTracker.getTrackCount(), "10-Track Count");
276         assertFalse(new File(path).exists(), "11-testFile exists " + showFailures());
277         assertTrue(fileCleaningTracker.exitWhenFinished, "12-exitWhenFinished");
278         assertFalse(fileCleaningTracker.reaper.isAlive(), "13-reaper.isAlive");
279     }
280 
281     @Test
282     void testFileCleanerExitWhenFinished2() throws Exception {
283         final String path = testFile.getPath();
284 
285         assertFalse(testFile.exists());
286         RandomAccessFile raf = createRandomAccessFile();
287         assertTrue(testFile.exists());
288 
289         assertEquals(0, fileCleaningTracker.getTrackCount());
290         fileCleaningTracker.track(path, raf);
291         assertEquals(1, fileCleaningTracker.getTrackCount());
292         assertFalse(fileCleaningTracker.exitWhenFinished);
293         assertTrue(fileCleaningTracker.reaper.isAlive());
294 
295         raf.close();
296         testFile = null;
297         raf = null;
298 
299         waitUntilTrackCount0();
300         pauseForDeleteToComplete(new File(path));
301 
302         assertEquals(0, fileCleaningTracker.getTrackCount());
303         assertFalse(new File(path).exists(), showFailures());
304         assertFalse(fileCleaningTracker.exitWhenFinished);
305         assertTrue(fileCleaningTracker.reaper.isAlive());
306 
307         assertFalse(fileCleaningTracker.exitWhenFinished);
308         fileCleaningTracker.exitWhenFinished();
309         for (int i = 0; i < 20 && fileCleaningTracker.reaper.isAlive(); i++) {
310             TestUtils.sleep(500L);  // allow reaper thread to die
311         }
312         assertTrue(fileCleaningTracker.exitWhenFinished);
313         assertFalse(fileCleaningTracker.reaper.isAlive());
314         assertFalse(Files.exists(Paths.get(path)));
315     }
316 
317     @Test
318     void testFileCleanerExitWhenFinishedFirst() throws Exception {
319         assertFalse(fileCleaningTracker.exitWhenFinished);
320         fileCleaningTracker.exitWhenFinished();
321         assertTrue(fileCleaningTracker.exitWhenFinished);
322         assertNull(fileCleaningTracker.reaper);
323 
324         waitUntilTrackCount0();
325 
326         assertEquals(0, fileCleaningTracker.getTrackCount());
327         assertTrue(fileCleaningTracker.exitWhenFinished);
328         assertNull(fileCleaningTracker.reaper);
329     }
330 
331     @Test
332     void testFileCleanerFile() throws Exception {
333         final String path = testFile.getPath();
334 
335         assertFalse(testFile.exists());
336         RandomAccessFile raf = createRandomAccessFile();
337         assertTrue(testFile.exists());
338 
339         assertEquals(0, fileCleaningTracker.getTrackCount());
340         fileCleaningTracker.track(path, raf);
341         assertEquals(1, fileCleaningTracker.getTrackCount());
342 
343         raf.close();
344         testFile = null;
345         raf = null;
346 
347         waitUntilTrackCount0();
348         pauseForDeleteToComplete(new File(path));
349 
350         assertEquals(0, fileCleaningTracker.getTrackCount());
351         assertFalse(new File(path).exists(), showFailures());
352     }
353 
354     @Test
355     void testFileCleanerNull() {
356         assertThrows(NullPointerException.class, () -> fileCleaningTracker.track((File) null, new Object()));
357         assertThrows(NullPointerException.class, () -> fileCleaningTracker.track((File) null, new Object(), FileDeleteStrategy.NORMAL));
358         assertThrows(NullPointerException.class, () -> fileCleaningTracker.track((String) null, new Object()));
359         assertThrows(NullPointerException.class, () -> fileCleaningTracker.track((String) null, new Object(), FileDeleteStrategy.NORMAL));
360     }
361 
362 
363     private void waitUntilTrackCount0() throws Exception {
364         System.gc();
365         TestUtils.sleep(500);
366         int count = 0;
367         while (fileCleaningTracker.getTrackCount() != 0 && count++ < 5) {
368             List<String> list = new ArrayList<>();
369             try {
370                 long i = 0;
371                 while (fileCleaningTracker.getTrackCount() != 0) {
372                     list.add(
373                             "A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String "
374                                     + i++);
375                 }
376             } catch (final Throwable ignored) {
377             }
378             list = null;
379             gcFinalize();
380             TestUtils.sleep(1000);
381         }
382         if (fileCleaningTracker.getTrackCount() != 0) {
383             throw new IllegalStateException("Your JVM is not releasing References, try running the test with less memory (-Xmx)");
384         }
385     }
386     }