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    *      http://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  
18  package org.apache.commons.io.file;
19  
20  import static org.apache.commons.io.file.CounterAssertions.assertCounts;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertNotEquals;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.IOException;
26  import java.nio.file.FileVisitResult;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.nio.file.attribute.BasicFileAttributes;
31  import java.time.Duration;
32  import java.util.ArrayList;
33  import java.util.LinkedHashSet;
34  import java.util.List;
35  import java.util.Set;
36  import java.util.concurrent.ExecutorService;
37  import java.util.concurrent.Executors;
38  import java.util.concurrent.TimeUnit;
39  import java.util.concurrent.atomic.AtomicBoolean;
40  import java.util.concurrent.atomic.AtomicInteger;
41  import java.util.function.Supplier;
42  import java.util.stream.Stream;
43  
44  import org.apache.commons.io.ThreadUtils;
45  import org.apache.commons.io.filefilter.AndFileFilter;
46  import org.apache.commons.io.filefilter.DirectoryFileFilter;
47  import org.apache.commons.io.filefilter.EmptyFileFilter;
48  import org.apache.commons.io.filefilter.PathVisitorFileFilter;
49  import org.apache.commons.io.filefilter.TrueFileFilter;
50  import org.junit.jupiter.api.Test;
51  import org.junit.jupiter.api.io.TempDir;
52  import org.junit.jupiter.params.ParameterizedTest;
53  import org.junit.jupiter.params.provider.Arguments;
54  import org.junit.jupiter.params.provider.MethodSource;
55  
56  /**
57   * Tests both {@link AccumulatorPathVisitor} and {@link PathVisitorFileFilter}.
58   */
59  public class AccumulatorPathVisitorTest {
60  
61      static Stream<Arguments> testParameters() {
62          // @formatter:off
63          return Stream.of(
64              Arguments.of((Supplier<AccumulatorPathVisitor>) AccumulatorPathVisitor::withLongCounters),
65              Arguments.of((Supplier<AccumulatorPathVisitor>) AccumulatorPathVisitor::withBigIntegerCounters),
66              Arguments.of((Supplier<AccumulatorPathVisitor>) () ->
67                  AccumulatorPathVisitor.withBigIntegerCounters(TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE)));
68          // @formatter:on
69      }
70  
71      static Stream<Arguments> testParametersIgnoreFailures() {
72          // @formatter:off
73          return Stream.of(
74              Arguments.of((Supplier<AccumulatorPathVisitor>) () -> new AccumulatorPathVisitor(
75                  Counters.bigIntegerPathCounters(),
76                  CountingPathVisitor.defaultDirFilter(),
77                  CountingPathVisitor.defaultFileFilter())));
78          // @formatter:on
79      }
80  
81      @TempDir
82      Path tempDirPath;
83  
84      /**
85       * Tests the 0-argument constructor.
86       */
87      @Test
88      public void test0ArgConstructor() throws IOException {
89          final AccumulatorPathVisitor accPathVisitor = new AccumulatorPathVisitor();
90          final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor);
91          Files.walkFileTree(tempDirPath, new AndFileFilter(countingFileFilter, DirectoryFileFilter.INSTANCE, EmptyFileFilter.EMPTY));
92          assertCounts(0, 0, 0, accPathVisitor.getPathCounters());
93          assertEquals(1, accPathVisitor.getDirList().size());
94          assertTrue(accPathVisitor.getFileList().isEmpty());
95          assertEquals(accPathVisitor, accPathVisitor);
96          assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
97      }
98  
99      /**
100      * Tests an empty folder.
101      */
102     @ParameterizedTest
103     @MethodSource("testParameters")
104     public void testEmptyFolder(final Supplier<AccumulatorPathVisitor> supplier) throws IOException {
105         final AccumulatorPathVisitor accPathVisitor = supplier.get();
106         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor);
107         Files.walkFileTree(tempDirPath, new AndFileFilter(countingFileFilter, DirectoryFileFilter.INSTANCE, EmptyFileFilter.EMPTY));
108         assertCounts(1, 0, 0, accPathVisitor.getPathCounters());
109         assertEquals(1, accPathVisitor.getDirList().size());
110         assertTrue(accPathVisitor.getFileList().isEmpty());
111         assertEquals(accPathVisitor, accPathVisitor);
112         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
113     }
114 
115     @Test
116     public void testEqualsHashCode() {
117         final AccumulatorPathVisitor visitor0 = AccumulatorPathVisitor.withLongCounters();
118         final AccumulatorPathVisitor visitor1 = AccumulatorPathVisitor.withLongCounters();
119         assertEquals(visitor0, visitor0);
120         assertEquals(visitor0, visitor1);
121         assertEquals(visitor1, visitor0);
122         assertEquals(visitor0.hashCode(), visitor0.hashCode());
123         assertEquals(visitor0.hashCode(), visitor1.hashCode());
124         assertEquals(visitor1.hashCode(), visitor0.hashCode());
125         visitor0.getPathCounters().getByteCounter().increment();
126         assertEquals(visitor0, visitor0);
127         assertNotEquals(visitor0, visitor1);
128         assertNotEquals(visitor1, visitor0);
129         assertEquals(visitor0.hashCode(), visitor0.hashCode());
130         assertNotEquals(visitor0.hashCode(), visitor1.hashCode());
131         assertNotEquals(visitor1.hashCode(), visitor0.hashCode());
132     }
133 
134     /**
135      * Tests a directory with one file of size 0.
136      */
137     @ParameterizedTest
138     @MethodSource("testParameters")
139     public void testFolders1FileSize0(final Supplier<AccumulatorPathVisitor> supplier) throws IOException {
140         final AccumulatorPathVisitor accPathVisitor = supplier.get();
141         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor);
142         Files.walkFileTree(Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0"), countingFileFilter);
143         assertCounts(1, 1, 0, accPathVisitor.getPathCounters());
144         assertEquals(1, accPathVisitor.getDirList().size());
145         assertEquals(1, accPathVisitor.getFileList().size());
146         assertEquals(accPathVisitor, accPathVisitor);
147         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
148     }
149 
150     /**
151      * Tests a directory with one file of size 1.
152      */
153     @ParameterizedTest
154     @MethodSource("testParameters")
155     public void testFolders1FileSize1(final Supplier<AccumulatorPathVisitor> supplier) throws IOException {
156         final AccumulatorPathVisitor accPathVisitor = supplier.get();
157         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor);
158         Files.walkFileTree(Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1"), countingFileFilter);
159         assertCounts(1, 1, 1, accPathVisitor.getPathCounters());
160         assertEquals(1, accPathVisitor.getDirList().size());
161         assertEquals(1, accPathVisitor.getFileList().size());
162         assertEquals(accPathVisitor, accPathVisitor);
163         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
164     }
165 
166     /**
167      * Tests a directory with two subdirectories, each containing one file of size 1.
168      */
169     @ParameterizedTest
170     @MethodSource("testParameters")
171     public void testFolders2FileSize2(final Supplier<AccumulatorPathVisitor> supplier) throws IOException {
172         final AccumulatorPathVisitor accPathVisitor = supplier.get();
173         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor);
174         Files.walkFileTree(Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-2"), countingFileFilter);
175         assertCounts(3, 2, 2, accPathVisitor.getPathCounters());
176         assertEquals(3, accPathVisitor.getDirList().size());
177         assertEquals(2, accPathVisitor.getFileList().size());
178         assertEquals(accPathVisitor, accPathVisitor);
179         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
180     }
181 
182     /**
183      * Tests IO-755 with a directory with 100 files, and delete all of them midway through the visit.
184      *
185      * Random failure like:
186      *
187      * <pre>
188      * ...?...
189      * </pre>
190      */
191     @ParameterizedTest
192     @MethodSource("testParametersIgnoreFailures")
193     public void testFolderWhileDeletingAsync(final Supplier<AccumulatorPathVisitor> supplier) throws IOException, InterruptedException {
194         final int count = 10_000;
195         final List<Path> files = new ArrayList<>(count);
196         // Create "count" file fixtures
197         for (int i = 1; i <= count; i++) {
198             final Path tempFile = Files.createTempFile(tempDirPath, "test", ".txt");
199             assertTrue(Files.exists(tempFile));
200             files.add(tempFile);
201         }
202         final AccumulatorPathVisitor accPathVisitor = supplier.get();
203         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor) {
204             @Override
205             public FileVisitResult visitFile(final Path path, final BasicFileAttributes attributes) throws IOException {
206                 // Slow down the walking a bit to try and cause conflicts with the deletion thread
207                 try {
208                     ThreadUtils.sleep(Duration.ofMillis(10));
209                 } catch (final InterruptedException ignore) {
210                     // e.printStackTrace();
211                 }
212                 return super.visitFile(path, attributes);
213             }
214         };
215         final ExecutorService executor = Executors.newSingleThreadExecutor();
216         final AtomicBoolean deleted = new AtomicBoolean();
217         try {
218             executor.execute(() -> {
219                 for (final Path file : files) {
220                     try {
221                         // File deletion is slow compared to tree walking, so we go as fast as we can here
222                         Files.delete(file);
223                     } catch (final IOException ignored) {
224                         // e.printStackTrace();
225                     }
226                 }
227                 deleted.set(true);
228             });
229             Files.walkFileTree(tempDirPath, countingFileFilter);
230         } finally {
231             if (!deleted.get()) {
232                 ThreadUtils.sleep(Duration.ofMillis(1000));
233             }
234             if (!deleted.get()) {
235                 executor.awaitTermination(5, TimeUnit.SECONDS);
236             }
237             executor.shutdownNow();
238         }
239         assertEquals(accPathVisitor, accPathVisitor);
240         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
241     }
242 
243     /**
244      * Tests IO-755 with a directory with 100 files, and delete all of them midway through the visit.
245      */
246     @ParameterizedTest
247     @MethodSource("testParametersIgnoreFailures")
248     public void testFolderWhileDeletingSync(final Supplier<AccumulatorPathVisitor> supplier) throws IOException {
249         final int count = 100;
250         final int marker = count / 2;
251         final Set<Path> files = new LinkedHashSet<>(count);
252         for (int i = 1; i <= count; i++) {
253             final Path tempFile = Files.createTempFile(tempDirPath, "test", ".txt");
254             assertTrue(Files.exists(tempFile));
255             files.add(tempFile);
256         }
257         final AccumulatorPathVisitor accPathVisitor = supplier.get();
258         final AtomicInteger visitCount = new AtomicInteger();
259         final PathVisitorFileFilter countingFileFilter = new PathVisitorFileFilter(accPathVisitor) {
260             @Override
261             public FileVisitResult visitFile(final Path path, final BasicFileAttributes attributes) throws IOException {
262                 if (visitCount.incrementAndGet() == marker) {
263                     // Now that we've visited half the files, delete them all
264                     for (final Path file : files) {
265                         Files.delete(file);
266                     }
267                 }
268                 return super.visitFile(path, attributes);
269             }
270         };
271         Files.walkFileTree(tempDirPath, countingFileFilter);
272         assertCounts(1, marker - 1, 0, accPathVisitor.getPathCounters());
273         assertEquals(1, accPathVisitor.getDirList().size());
274         assertEquals(marker - 1, accPathVisitor.getFileList().size());
275         assertEquals(accPathVisitor, accPathVisitor);
276         assertEquals(accPathVisitor.hashCode(), accPathVisitor.hashCode());
277     }
278 
279 }