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  
18  package org.apache.commons.io.channels;
19  
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.assertSame;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  
27  import java.io.IOException;
28  import java.nio.ByteBuffer;
29  import java.nio.channels.ClosedChannelException;
30  import java.nio.channels.SeekableByteChannel;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.util.Arrays;
34  import java.util.Random;
35  
36  import org.junit.jupiter.api.AfterEach;
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  import org.junit.jupiter.api.io.TempDir;
40  import org.junit.jupiter.params.ParameterizedTest;
41  import org.junit.jupiter.params.provider.CsvSource;
42  import org.junit.jupiter.params.provider.ValueSource;
43  
44  /**
45   * Comprehensive test suite for SeekableByteChannel implementations. Tests can be run against any SeekableByteChannel implementation by overriding the
46   * createChannel() method in a subclass.
47   */
48  abstract class AbstractSeekableByteChannelTest {
49  
50      private SeekableByteChannel channel;
51  
52      @TempDir
53      protected Path tempDir;
54  
55      protected Path tempFile;
56  
57      private final Random random = new Random(42);
58  
59      /**
60       * Creates the SeekableByteChannel to test.
61       *
62       * @return a new SeekableByteChannel.
63       * @throws IOException Thrown when the SeekableByteChannel cannot be created.
64       */
65      protected abstract SeekableByteChannel createChannel() throws IOException;
66  
67      @BeforeEach
68      void setUp() throws IOException {
69          tempFile = tempDir.resolve(getClass().getSimpleName() + ".tmp");
70          channel = createChannel();
71      }
72  
73      @AfterEach
74      void tearDown() throws IOException {
75          if (channel != null && channel.isOpen()) {
76              channel.close();
77          }
78          if (tempFile != null && Files.exists(tempFile)) {
79              Files.delete(tempFile);
80          }
81      }
82  
83      @Test
84      void testCloseMultipleTimes() throws IOException {
85          channel.close();
86          channel.close(); // Should not throw
87          assertFalse(channel.isOpen());
88      }
89  
90      @Test
91      void testConcurrentPositionAndSizeQueries() throws IOException {
92          final byte[] data = "test data".getBytes();
93          channel.write(ByteBuffer.wrap(data));
94          final long size = channel.size();
95          final long position = channel.position();
96          assertEquals(data.length, size);
97          assertEquals(data.length, position);
98          // These values should be consistent
99          assertEquals(channel.size(), size);
100         assertEquals(channel.position(), position);
101     }
102 
103     @Test
104     void testIsOpenAfterClose() throws IOException {
105         channel.close();
106         assertFalse(channel.isOpen());
107     }
108 
109     @Test
110     void testIsOpennOnNew() {
111         assertTrue(channel.isOpen());
112     }
113 
114     @Test
115     void testPartialWritesAndReads() throws IOException {
116         final byte[] data = "0123456789".getBytes();
117         // Write in chunks
118         channel.write(ByteBuffer.wrap(data, 0, 5));
119         channel.write(ByteBuffer.wrap(data, 5, 5));
120         assertEquals(10, channel.size());
121         // Read back in different chunks
122         channel.position(0);
123         final ByteBuffer buffer1 = ByteBuffer.allocate(3);
124         final ByteBuffer buffer2 = ByteBuffer.allocate(7);
125         assertEquals(3, channel.read(buffer1));
126         assertEquals(7, channel.read(buffer2));
127         assertArrayEquals(Arrays.copyOf(data, 3), buffer1.array());
128         assertArrayEquals(Arrays.copyOfRange(data, 3, 10), buffer2.array());
129     }
130 
131     @Test
132     void testPositionBeyondSize() throws IOException {
133         channel.write(ByteBuffer.wrap("test".getBytes()));
134         channel.position(100);
135         assertEquals(100, channel.position());
136         assertEquals(4, channel.size()); // Size should not change
137     }
138 
139     @ParameterizedTest
140     @CsvSource({ "0, 0", "5, 5", "10, 10", "100, 100" })
141     void testPositionInBounds(final long newPosition, final long expectedPosition) throws IOException {
142         // Create file with enough data
143         final byte[] data = new byte[200];
144         random.nextBytes(data);
145         channel.write(ByteBuffer.wrap(data));
146         @SuppressWarnings("resource") // returns "this".
147         final SeekableByteChannel result = channel.position(newPosition);
148         assertSame(channel, result); // Javadoc: "This channel"
149         assertEquals(expectedPosition, channel.position());
150     }
151 
152     @Test
153     void testPositionNegative() {
154         assertThrows(IllegalArgumentException.class, () -> channel.position(-1));
155     }
156 
157     @Test
158     void testPositionOnClosed() throws IOException {
159         channel.close();
160         assertThrows(ClosedChannelException.class, () -> channel.position());
161         assertThrows(ClosedChannelException.class, () -> channel.position(0));
162     }
163 
164     @Test
165     void testPositionOnNew() throws IOException {
166         assertEquals(0, channel.position());
167     }
168 
169     @Test
170     void testRandomAccess() throws IOException {
171         final byte[] data = new byte[1000];
172         random.nextBytes(data);
173         // Write initial data
174         channel.write(ByteBuffer.wrap(data));
175         // Perform random access operations
176         final int[] positions = { 100, 500, 0, 999, 250 };
177         for (final int pos : positions) {
178             channel.position(pos);
179             assertEquals(pos, channel.position());
180             final ByteBuffer buffer = ByteBuffer.allocate(1);
181             final int read = channel.read(buffer);
182             if (pos < data.length) {
183                 assertEquals(1, read);
184                 assertEquals(data[pos], buffer.get(0));
185             }
186         }
187     }
188 
189     @Test
190     void testReadAtEndOfFile() throws IOException {
191         channel.write(ByteBuffer.wrap("test".getBytes()));
192         // Position is already at end after write
193         final ByteBuffer buffer = ByteBuffer.allocate(10);
194         final int read = channel.read(buffer);
195         assertEquals(-1, read);
196     }
197 
198     @Test
199     void testReadBuffer() throws IOException {
200         final byte[] data = "test".getBytes();
201         channel.write(ByteBuffer.wrap(data));
202         channel.position(0);
203         final ByteBuffer buffer = ByteBuffer.allocate(100);
204         final int read = channel.read(buffer);
205         assertEquals(data.length, read);
206         assertEquals(data.length, channel.position());
207         // Verify only the expected bytes were read
208         final byte[] readData = new byte[read];
209         buffer.flip();
210         buffer.get(readData);
211         assertArrayEquals(data, readData);
212     }
213 
214     @Test
215     void testReadBytes() throws IOException {
216         final byte[] data = "Hello, World!".getBytes();
217         channel.write(ByteBuffer.wrap(data));
218         channel.position(0);
219         final ByteBuffer buffer = ByteBuffer.allocate(data.length);
220         final int read = channel.read(buffer);
221         assertEquals(data.length, read);
222         assertArrayEquals(data, buffer.array());
223         assertEquals(data.length, channel.position());
224     }
225 
226     @Test
227     void testReadClosed() throws IOException {
228         channel.close();
229         final ByteBuffer buffer = ByteBuffer.allocate(10);
230         assertThrows(ClosedChannelException.class, () -> channel.read(buffer));
231     }
232 
233     @Test
234     void testReadEmpty() throws IOException {
235         final ByteBuffer buffer = ByteBuffer.allocate(10);
236         final int read = channel.read(buffer);
237         assertEquals(-1, read);
238         assertEquals(0, buffer.position());
239     }
240 
241     @Test
242     void testReadNull() {
243         assertThrows(NullPointerException.class, () -> channel.read(null));
244     }
245 
246     @Test
247     void testReadSingleByte() throws IOException {
248         // Write first
249         channel.write(ByteBuffer.wrap(new byte[] { 42 }));
250         channel.position(0);
251         final ByteBuffer buffer = ByteBuffer.allocate(1);
252         final int read = channel.read(buffer);
253         assertEquals(1, read);
254         assertEquals(42, buffer.get(0));
255         assertEquals(1, channel.position());
256     }
257 
258     @Test
259     void testSizeAfterTruncateToLargerSize() throws IOException {
260         channel.write(ByteBuffer.wrap("Hello".getBytes()));
261         assertEquals(5, channel.size());
262         channel.truncate(10);
263         assertEquals(5, channel.size()); // Size should remain unchanged
264     }
265 
266     @Test
267     void testSizeAfterWrite() throws IOException {
268         assertEquals(0, channel.size());
269         channel.write(ByteBuffer.wrap("Hello".getBytes()));
270         assertEquals(5, channel.size());
271         channel.write(ByteBuffer.wrap(" World".getBytes()));
272         assertEquals(11, channel.size());
273     }
274 
275     @Test
276     void testSizeOnClosed() throws IOException {
277         channel.close();
278         assertThrows(ClosedChannelException.class, () -> channel.size());
279     }
280 
281     @Test
282     void testSizeOnNew() throws IOException {
283         assertEquals(0, channel.size());
284     }
285 
286     @Test
287     void testSizeSameOnOverwrite() throws IOException {
288         channel.write(ByteBuffer.wrap("Hello World".getBytes()));
289         assertEquals(11, channel.size());
290         channel.position(6);
291         channel.write(ByteBuffer.wrap("Test".getBytes()));
292         assertEquals(11, channel.size()); // Size should not change
293     }
294 
295     @Test
296     void testTruncateNegative() {
297         assertThrows(IllegalArgumentException.class, () -> channel.truncate(-1));
298     }
299 
300     @Test
301     void testTruncateShrinks() throws IOException {
302         channel.write(ByteBuffer.wrap("Hello World".getBytes()));
303         assertEquals(11, channel.size());
304         channel.truncate(5);
305         assertEquals(5, channel.size());
306         // Position should be adjusted if it was beyond new size
307         if (channel.position() > 5) {
308             assertEquals(5, channel.position());
309         }
310     }
311 
312     @Test
313     void testWriteBeyondSizeGrows() throws IOException {
314         channel.position(100);
315         final byte[] data = "test".getBytes();
316         channel.write(ByteBuffer.wrap(data));
317         assertEquals(104, channel.size());
318         assertEquals(104, channel.position());
319         // Verify the gap contains zeros (implementation dependent)
320         channel.position(0);
321         final ByteBuffer buffer = ByteBuffer.allocate(100);
322         channel.read(buffer);
323         // Most implementations will fill gaps with zeros
324         final byte[] expectedGap = new byte[100];
325         assertArrayEquals(expectedGap, buffer.array());
326     }
327 
328     @ParameterizedTest
329     @ValueSource(ints = { 1, 10, 100, 1000, 10000 })
330     void testWriteDifferentSizes(final int size) throws IOException {
331         final byte[] data = new byte[size];
332         random.nextBytes(data);
333         final ByteBuffer buffer = ByteBuffer.wrap(data);
334         final int byteCount = channel.write(buffer);
335         assertEquals(size, byteCount);
336         assertEquals(size, channel.position());
337         assertEquals(size, channel.size());
338     }
339     @Test
340     void testWriteEmpty() throws IOException {
341         final ByteBuffer buffer = ByteBuffer.allocate(0);
342         final int byteCount = channel.write(buffer);
343         assertEquals(0, byteCount);
344         assertEquals(0, channel.position());
345         assertEquals(0, channel.size());
346     }
347     @Test
348     void testWriteNull() {
349         assertThrows(NullPointerException.class, () -> channel.write(null));
350     }
351     @Test
352     void testWritePositionReadVerify() throws IOException {
353         final byte[] originalData = "Hello, SeekableByteChannel World!".getBytes();
354         // Write data
355         channel.write(ByteBuffer.wrap(originalData));
356         // Seek to beginning
357         channel.position(0);
358         // Read data back
359         final ByteBuffer readBuffer = ByteBuffer.allocate(originalData.length);
360         final int bytesRead = channel.read(readBuffer);
361         assertEquals(originalData.length, bytesRead);
362         assertArrayEquals(originalData, readBuffer.array());
363     }
364 
365     @Test
366     void testWriteSingleByte() throws IOException {
367         final ByteBuffer buffer = ByteBuffer.allocate(1);
368         buffer.put((byte) 42);
369         buffer.flip();
370         final int written = channel.write(buffer);
371         assertEquals(1, written);
372         assertEquals(1, channel.position());
373         assertEquals(1, channel.size());
374     }
375 
376     @Test
377     void testWriteToClosedChannel() throws IOException {
378         channel.close();
379         final ByteBuffer buffer = ByteBuffer.wrap("test".getBytes());
380         assertThrows(ClosedChannelException.class, () -> channel.write(buffer));
381     }
382 
383     @Test
384     void tesWriteBytes() throws IOException {
385         final byte[] data = "Hello, World!".getBytes();
386         final ByteBuffer buffer = ByteBuffer.wrap(data);
387         final int byteCount = channel.write(buffer);
388         assertEquals(data.length, byteCount);
389         assertEquals(data.length, channel.position());
390         assertEquals(data.length, channel.size());
391     }
392 }