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.output;
18  
19  import static org.apache.commons.io.output.ThresholdingOutputStreamTest.assertThresholdingInitialState;
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertThrows;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.util.stream.IntStream;
34  
35  import org.apache.commons.io.IOUtils;
36  import org.apache.commons.io.file.AbstractTempDirTest;
37  import org.junit.jupiter.api.Test;
38  import org.junit.jupiter.api.io.TempDir;
39  import org.junit.jupiter.params.ParameterizedTest;
40  import org.junit.jupiter.params.provider.MethodSource;
41  
42  /**
43   * Tests {@code DeferredFileOutputStream}. See also the superclass {@link ThresholdingOutputStream}.
44   *
45   * @see ThresholdingOutputStream
46   */
47  class DeferredFileOutputStreamTest extends AbstractTempDirTest {
48  
49      private static void assertDeferredInitialState(final DeferredFileOutputStream out) {
50          assertTrue(out.isInMemory());
51      }
52  
53      public static IntStream data() {
54          return IntStream.of(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096);
55      }
56  
57      /**
58       * The test data as a string (which is the simplest form).
59       */
60      private final String testString = "0123456789";
61  
62      /**
63       * The test data as a byte array, derived from the string.
64       */
65      private final byte[] testBytes = testString.getBytes();
66  
67      /**
68       * Tests the case where the amount of data exceeds the threshold, and is therefore written to disk. The actual data
69       * written to disk is verified, as is the file itself.
70       *
71       * @param initialBufferSize the initial buffer size.
72       * @throws IOException on a test failure.
73       */
74      @ParameterizedTest(name = "initialBufferSize = {0}")
75      @MethodSource("data")
76      void testAboveThreshold(final int initialBufferSize) throws IOException {
77          final File testFile = Files.createTempFile(tempDirPath, "testAboveThreshold", "dat").toFile();
78          final int threshold = testBytes.length - 5;
79          try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
80                  .setThreshold(threshold)
81                  .setBufferSize(initialBufferSize)
82                  .setOutputFile(testFile)
83                  .get()) {
84              assertThresholdingInitialState(out, threshold, 0);
85              assertDeferredInitialState(out);
86              out.write(testBytes, 0, testBytes.length);
87              out.close();
88              assertFalse(out.isInMemory());
89              assertNull(out.getData());
90              assertEquals(testFile.length(), out.getByteCount());
91              verifyResultFile(testFile);
92          }
93      }
94  
95      /**
96       * Tests the case where the amount of data exceeds the threshold, and is therefore written to disk. The actual data
97       * written to disk is verified, as is the file itself.
98       * Testing the getInputStream() method.
99       */
100     @ParameterizedTest(name = "initialBufferSize = {0}")
101     @MethodSource("data")
102     void testAboveThresholdGetInputStream(final int initialBufferSize, final @TempDir Path tempDir) throws IOException {
103         final File testFile = Files.createTempFile(tempDirPath, "testAboveThreshold", "dat").toFile();
104         final int threshold = testBytes.length - 5;
105         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
106                 .setThreshold(threshold)
107                 .setBufferSize(initialBufferSize)
108                 .setOutputFile(testFile)
109                 .get()) {
110             assertThresholdingInitialState(out, threshold, 0);
111             assertDeferredInitialState(out);
112             out.write(testBytes, 0, testBytes.length);
113             out.close();
114             assertFalse(out.isInMemory());
115             assertEquals(testFile.length(), out.getByteCount());
116             try (InputStream is = out.toInputStream()) {
117                 assertArrayEquals(testBytes, IOUtils.toByteArray(is));
118             }
119             verifyResultFile(testFile);
120         }
121     }
122 
123     /**
124      * Tests the case where the amount of data is exactly the same as the threshold. The behavior should be the same as
125      * that for the amount of data being below (not exceeding) the threshold.
126      */
127     @ParameterizedTest(name = "initialBufferSize = {0}")
128     @MethodSource("data")
129     void testAtThreshold(final int initialBufferSize) throws IOException {
130         final int threshold = testBytes.length;
131         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
132             // @formatter:off
133                 .setThreshold(threshold)
134                 .setBufferSize(initialBufferSize)
135                 .get()) {
136             // @formatter:on
137             assertThresholdingInitialState(out, threshold, 0);
138             assertDeferredInitialState(out);
139             out.write(testBytes, 0, testBytes.length);
140             out.close();
141             assertTrue(out.isInMemory());
142             assertEquals(testBytes.length, out.getByteCount());
143             final byte[] resultBytes = out.getData();
144             assertEquals(testBytes.length, resultBytes.length);
145             assertArrayEquals(resultBytes, testBytes);
146         }
147     }
148 
149     /**
150      * Tests the case where the amount of data falls below the threshold, and is therefore confined to memory.
151      * @throws IOException
152      */
153     @ParameterizedTest(name = "initialBufferSize = {0}")
154     @MethodSource("data")
155     void testBelowThreshold(final int initialBufferSize) throws IOException {
156         final int threshold = testBytes.length + 42;
157         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
158             // @formatter:off
159                 .setThreshold(threshold)
160                 .setBufferSize(initialBufferSize)
161                 .get()) {
162             // @formatter:on
163             assertThresholdingInitialState(out, threshold, 0);
164             assertDeferredInitialState(out);
165             out.write(testBytes, 0, testBytes.length);
166             out.close();
167             assertTrue(out.isInMemory());
168             assertEquals(testBytes.length, out.getByteCount());
169             final byte[] resultBytes = out.getData();
170             assertEquals(testBytes.length, resultBytes.length);
171             assertArrayEquals(resultBytes, testBytes);
172         }
173     }
174 
175     /**
176      * Tests the case where the amount of data falls below the threshold, and is therefore confined to memory.
177      * Testing the getInputStream() method.
178      */
179     @ParameterizedTest(name = "initialBufferSize = {0}")
180     @MethodSource("data")
181     void testBelowThresholdGetInputStream(final int initialBufferSize) throws IOException {
182         // @formatter:off
183         final int threshold = testBytes.length + 42;
184         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
185                 .setThreshold(threshold)
186                 .setBufferSize(initialBufferSize)
187                 .get()) {
188         // @formatter:on
189             assertThresholdingInitialState(out, threshold, 0);
190             assertDeferredInitialState(out);
191             out.write(testBytes, 0, testBytes.length);
192             out.close();
193             assertTrue(out.isInMemory());
194             assertEquals(testBytes.length, out.getByteCount());
195             try (InputStream is = out.toInputStream()) {
196                 assertArrayEquals(testBytes, IOUtils.toByteArray(is));
197             }
198         }
199     }
200 
201     /**
202      * Tests specifying a temporary file and the threshold is reached.
203      */
204     @ParameterizedTest(name = "initialBufferSize = {0}")
205     @MethodSource("data")
206     void testTempFileAboveThreshold(final int initialBufferSize) throws IOException {
207         final String prefix = "commons-io-test";
208         final String suffix = ".out";
209         // @formatter:off
210         final int threshold = testBytes.length - 5;
211         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
212                 .setThreshold(threshold)
213                 .setBufferSize(initialBufferSize)
214                 .setPrefix(prefix)
215                 .setSuffix(suffix)
216                 .setDirectory(tempDirFile)
217                 .setDirectory(tempDirPath.toFile())
218                 .get()) {
219         // @formatter:on
220             assertThresholdingInitialState(out, threshold, 0);
221             assertDeferredInitialState(out);
222             assertNull(out.getFile(), "Check File is null-A");
223             assertNull(out.getPath(), "Check Path is null-A");
224             out.write(testBytes, 0, testBytes.length);
225             out.close();
226             assertFalse(out.isInMemory());
227             assertEquals(testBytes.length, out.getByteCount());
228             assertNull(out.getData());
229             assertNotNull(out.getFile(), "Check file not null");
230             assertTrue(out.getFile().exists(), "Check file exists");
231             assertTrue(out.getFile().getName().startsWith(prefix), "Check prefix");
232             assertTrue(out.getFile().getName().endsWith(suffix), "Check suffix");
233             assertEquals(tempDirPath, out.getPath().getParent(), "Check dir");
234             verifyResultFile(out.getFile());
235         }
236     }
237 
238     /**
239      * Tests specifying a temporary file and the threshold is reached.
240      * @throws IOException
241      */
242     @ParameterizedTest(name = "initialBufferSize = {0}")
243     @MethodSource("data")
244     void testTempFileAboveThresholdPrefixOnly(final int initialBufferSize) throws IOException {
245         final String prefix = "commons-io-test";
246         final String suffix = null;
247         final int threshold = testBytes.length - 5;
248         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
249             // @formatter:off
250                 .setThreshold(threshold)
251                 .setBufferSize(initialBufferSize)
252                 .setPrefix(prefix)
253                 .setSuffix(suffix)
254                 .setDirectory((Path) null)
255                 .get()) {
256             // @formatter:on
257             try {
258                 assertThresholdingInitialState(out, threshold, 0);
259                 assertDeferredInitialState(out);
260                 assertNull(out.getFile(), "Check File is null-A");
261                 assertNull(out.getPath(), "Check Path is null-A");
262                 out.write(testBytes, 0, testBytes.length);
263                 out.close();
264                 assertFalse(out.isInMemory());
265                 assertNull(out.getData());
266                 assertEquals(testBytes.length, out.getByteCount());
267                 assertNotNull(out.getFile(), "Check file not null");
268                 assertTrue(out.getFile().exists(), "Check file exists");
269                 assertTrue(out.getFile().getName().startsWith(prefix), "Check prefix");
270                 assertTrue(out.getFile().getName().endsWith(".tmp"), "Check suffix"); // ".tmp" is default
271                 verifyResultFile(out.getFile());
272             } finally {
273                 // Delete the temporary file.
274                 out.getFile().delete();
275             }
276         }
277     }
278 
279     /**
280      * Tests specifying a temporary file and the threshold not reached.
281      * @throws IOException
282      */
283     @ParameterizedTest(name = "initialBufferSize = {0}")
284     @MethodSource("data")
285     void testTempFileBelowThreshold(final int initialBufferSize) throws IOException {
286         final String prefix = "commons-io-test";
287         final String suffix = ".out";
288         final int threshold = testBytes.length + 42;
289         try (DeferredFileOutputStream out = new DeferredFileOutputStream(threshold, initialBufferSize, prefix, suffix, tempDirFile)) {
290             assertThresholdingInitialState(out, threshold, 0);
291             assertDeferredInitialState(out);
292             assertNull(out.getFile(), "Check File is null-A");
293             assertNull(out.getPath(), "Check Path is null-A");
294             out.write(testBytes, 0, testBytes.length);
295             out.close();
296             assertTrue(out.isInMemory());
297             assertEquals(testBytes.length, out.getByteCount());
298             assertNull(out.getFile(), "Check file is null-B");
299         }
300     }
301 
302     /**
303      * Tests specifying a temporary file and the threshold is reached.
304      *
305      * @throws Exception
306      */
307     @Test
308     void testTempFileError() throws Exception {
309         final String prefix = null;
310         final String suffix = ".out";
311         assertThrows(NullPointerException.class, () -> new DeferredFileOutputStream(testBytes.length - 5, prefix, suffix, tempDirFile));
312     }
313 
314     /**
315      * Tests the case where the threshold is negative, and therefore the data is always written to disk. The actual data
316      * written to disk is verified, as is the file itself.
317      */
318     @ParameterizedTest(name = "initialBufferSize = {0}")
319     @MethodSource("data")
320     void testThresholdNegative(final int initialBufferSize) throws IOException {
321         final File testFile = Files.createTempFile(tempDirPath, "testThresholdNegative", "dat").toFile();
322         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
323                 .setThreshold(-1)
324                 .setBufferSize(initialBufferSize)
325                 .setOutputFile(testFile)
326                 .get()) {
327             assertThresholdingInitialState(out, 0, 0);
328             assertDeferredInitialState(out);
329             out.write(testBytes, 0, testBytes.length);
330             out.close();
331             assertFalse(out.isInMemory());
332             assertNull(out.getData());
333             assertEquals(testFile.length(), out.getByteCount());
334             verifyResultFile(testFile);
335         }
336     }
337 
338     /**
339      * Tests the case where there are multiple writes beyond the threshold, to ensure that the
340      * {@code thresholdReached()} method is only called once, as the threshold is crossed for the first time.
341      * @throws IOException
342      */
343     @ParameterizedTest(name = "initialBufferSize = {0}")
344     @MethodSource("data")
345     void testThresholdReached(final int initialBufferSize) throws IOException {
346         final File testFile = Files.createTempFile(tempDirPath, "testThresholdReached", "dat").toFile();
347         final int threshold = testBytes.length / 2;
348         try (DeferredFileOutputStream out = DeferredFileOutputStream.builder()
349             // @formatter:off
350                 .setThreshold(threshold)
351                 .setBufferSize(initialBufferSize)
352                 .setOutputFile(testFile)
353                 .get()) {
354             // @formatter:on
355             assertThresholdingInitialState(out, threshold, 0);
356             assertDeferredInitialState(out);
357             final int chunkSize = testBytes.length / 3;
358             out.write(testBytes, 0, chunkSize);
359             out.write(testBytes, chunkSize, chunkSize);
360             out.write(testBytes, chunkSize * 2, testBytes.length - chunkSize * 2);
361             out.close();
362             assertFalse(out.isInMemory());
363             assertNull(out.getData());
364             assertEquals(testBytes.length, out.getByteCount());
365             verifyResultFile(testFile);
366         }
367     }
368 
369     /**
370      * Tests whether writeTo() properly writes large content.
371      */
372     @ParameterizedTest(name = "initialBufferSize = {0}")
373     @MethodSource("data")
374     void testWriteToLarge(final int initialBufferSize) throws IOException {
375         final File testFile = Files.createTempFile(tempDirPath, "testWriteToFile", "dat").toFile();
376         final int threshold = testBytes.length / 2;
377         try (ByteArrayOutputStream baos = new ByteArrayOutputStream(initialBufferSize);
378                 DeferredFileOutputStream dfos = DeferredFileOutputStream.builder().setThreshold(threshold).setOutputFile(testFile).get()) {
379             assertThresholdingInitialState(dfos, threshold, 0);
380             assertDeferredInitialState(dfos);
381             dfos.write(testBytes);
382             assertTrue(testFile.exists());
383             assertFalse(dfos.isInMemory());
384             assertEquals(testBytes.length, dfos.getByteCount());
385             assertThrows(IOException.class, () -> dfos.writeTo(baos));
386             dfos.close();
387             dfos.writeTo(baos);
388             final byte[] copiedBytes = baos.toByteArray();
389             assertArrayEquals(testBytes, copiedBytes);
390             verifyResultFile(testFile);
391         }
392     }
393 
394     /**
395      * Tests whether writeTo() properly writes large content.
396      */
397     @ParameterizedTest(name = "initialBufferSize = {0}")
398     @MethodSource("data")
399     void testWriteToLargeCtor(final int initialBufferSize) throws IOException {
400         final File testFile = Files.createTempFile(tempDirPath, "testWriteToFile", "dat").toFile();
401         final int threshold = testBytes.length / 2;
402         try (ByteArrayOutputStream baos = new ByteArrayOutputStream(initialBufferSize);
403                 DeferredFileOutputStream dfos = new DeferredFileOutputStream(threshold, testFile)) {
404             assertThresholdingInitialState(dfos, threshold, 0);
405             assertDeferredInitialState(dfos);
406             dfos.write(testBytes);
407             assertTrue(testFile.exists());
408             assertFalse(dfos.isInMemory());
409             assertThrows(IOException.class, () -> dfos.writeTo(baos));
410             assertEquals(testBytes.length, dfos.getByteCount());
411             dfos.close();
412             dfos.writeTo(baos);
413             final byte[] copiedBytes = baos.toByteArray();
414             assertArrayEquals(testBytes, copiedBytes);
415             verifyResultFile(testFile);
416         }
417     }
418 
419     /**
420      * Tests whether writeTo() properly writes small content.
421      * @throws IOException
422      */
423     @ParameterizedTest(name = "initialBufferSize = {0}")
424     @MethodSource("data")
425     void testWriteToSmall(final int initialBufferSize) throws IOException {
426         final File testFile = Files.createTempFile(tempDirPath, "testWriteToMem", "dat").toFile();
427         final int threshold = testBytes.length * 2;
428         try (ByteArrayOutputStream baos = new ByteArrayOutputStream(initialBufferSize);
429                 DeferredFileOutputStream dfos = new DeferredFileOutputStream(threshold, initialBufferSize, testFile)) {
430             assertThresholdingInitialState(dfos, threshold, 0);
431             assertDeferredInitialState(dfos);
432             dfos.write(testBytes);
433             assertTrue(dfos.isInMemory());
434             assertThrows(IOException.class, () -> dfos.writeTo(baos));
435             assertEquals(testBytes.length, dfos.getByteCount());
436             dfos.close();
437             dfos.writeTo(baos);
438             final byte[] copiedBytes = baos.toByteArray();
439             assertArrayEquals(testBytes, copiedBytes);
440         }
441     }
442 
443     /**
444      * Verifies that the specified file contains the same data as the original test data.
445      *
446      * @param testFile The file containing the test output.
447      */
448     private void verifyResultFile(final File testFile) throws IOException {
449         try (InputStream fis = Files.newInputStream(testFile.toPath())) {
450             assertEquals(testBytes.length, fis.available());
451 
452             final byte[] resultBytes = new byte[testBytes.length];
453             assertEquals(testBytes.length, fis.read(resultBytes));
454 
455             assertArrayEquals(resultBytes, testBytes);
456             assertEquals(-1, fis.read(resultBytes));
457         }
458     }
459 }