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      protected 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  
91      @Test
92      void testConcurrentPositionAndSizeQueries() throws IOException {
93          final byte[] data = "test data".getBytes();
94          channel.write(ByteBuffer.wrap(data));
95          final long size = channel.size();
96          final long position = channel.position();
97          assertEquals(data.length, size);
98          assertEquals(data.length, position);
99          // These values should be consistent
100         assertEquals(channel.size(), size);
101         assertEquals(channel.position(), position);
102     }
103 
104     @Test
105     void testIsOpenAfterClose() throws IOException {
106         channel.close();
107         assertFalse(channel.isOpen());
108     }
109 
110     @Test
111     void testIsOpennOnNew() {
112         assertTrue(channel.isOpen());
113     }
114 
115     @Test
116     void testPartialWritesAndReads() throws IOException {
117         final byte[] data = "0123456789".getBytes();
118         // Write in chunks
119         channel.write(ByteBuffer.wrap(data, 0, 5));
120         channel.write(ByteBuffer.wrap(data, 5, 5));
121         assertEquals(10, channel.size());
122         // Read back in different chunks
123         channel.position(0);
124         final ByteBuffer buffer1 = ByteBuffer.allocate(3);
125         final ByteBuffer buffer2 = ByteBuffer.allocate(7);
126         assertEquals(3, channel.read(buffer1));
127         assertEquals(7, channel.read(buffer2));
128         assertArrayEquals(Arrays.copyOf(data, 3), buffer1.array());
129         assertArrayEquals(Arrays.copyOfRange(data, 3, 10), buffer2.array());
130     }
131 
132     @Test
133     void testPositionBeyondSize() throws IOException {
134         channel.write(ByteBuffer.wrap("test".getBytes()));
135         channel.position(100);
136         assertEquals(100, channel.position());
137         assertEquals(4, channel.size()); // Size should not change
138     }
139 
140     @Test
141     void testPositionBeyondSizeRead() throws IOException {
142         final ByteBuffer buffer = ByteBuffer.allocate(1);
143         channel.position(channel.size() + 1);
144         assertEquals(channel.size() + 1, channel.position());
145         assertEquals(-1, channel.read(buffer));
146         channel.position(Integer.MAX_VALUE + 1L);
147         assertEquals(Integer.MAX_VALUE + 1L, channel.position());
148         assertEquals(-1, channel.read(buffer));
149         assertThrows(IllegalArgumentException.class, () -> channel.position(-1));
150         assertThrows(IllegalArgumentException.class, () -> channel.position(Integer.MIN_VALUE));
151         assertThrows(IllegalArgumentException.class, () -> channel.position(Long.MIN_VALUE));
152     }
153 
154     @ParameterizedTest
155     @CsvSource({ "0, 0", "5, 5", "10, 10", "100, 100" })
156     void testPositionInBounds(final long newPosition, final long expectedPosition) throws IOException {
157         // Create file with enough data
158         final byte[] data = new byte[200];
159         random.nextBytes(data);
160         channel.write(ByteBuffer.wrap(data));
161         @SuppressWarnings("resource") // returns "this".
162         final SeekableByteChannel result = channel.position(newPosition);
163         assertSame(channel, result); // Javadoc: "This channel"
164         assertEquals(expectedPosition, channel.position());
165     }
166 
167 
168     @Test
169     void testPositionNegative() {
170         assertThrows(IllegalArgumentException.class, () -> channel.position(-1));
171     }
172 
173     @Test
174     void testPositionOnClosed() throws IOException {
175         channel.close();
176         assertThrows(ClosedChannelException.class, () -> channel.position());
177         assertThrows(ClosedChannelException.class, () -> channel.position(0));
178     }
179 
180     @Test
181     void testPositionOnNew() throws IOException {
182         assertEquals(0, channel.position());
183     }
184 
185     @Test
186     void testRandomAccess() throws IOException {
187         final byte[] data = new byte[1000];
188         random.nextBytes(data);
189         // Write initial data
190         channel.write(ByteBuffer.wrap(data));
191         // Perform random access operations
192         final int[] positions = { 100, 500, 0, 999, 250 };
193         for (final int pos : positions) {
194             channel.position(pos);
195             assertEquals(pos, channel.position());
196             final ByteBuffer buffer = ByteBuffer.allocate(1);
197             final int read = channel.read(buffer);
198             if (pos < data.length) {
199                 assertEquals(1, read);
200                 assertEquals(data[pos], buffer.get(0));
201             }
202         }
203     }
204 
205     @Test
206     void testReadAtEndOfFile() throws IOException {
207         channel.write(ByteBuffer.wrap("test".getBytes()));
208         // Position is already at end after write
209         final ByteBuffer buffer = ByteBuffer.allocate(10);
210         final int read = channel.read(buffer);
211         assertEquals(-1, read);
212     }
213 
214     @Test
215     void testReadBuffer() throws IOException {
216         final byte[] data = "test".getBytes();
217         channel.write(ByteBuffer.wrap(data));
218         channel.position(0);
219         final ByteBuffer buffer = ByteBuffer.allocate(100);
220         final int read = channel.read(buffer);
221         assertEquals(data.length, read);
222         assertEquals(data.length, channel.position());
223         // Verify only the expected bytes were read
224         final byte[] readData = new byte[read];
225         buffer.flip();
226         buffer.get(readData);
227         assertArrayEquals(data, readData);
228     }
229 
230     @Test
231     void testReadBytes() throws IOException {
232         final byte[] data = "Hello, World!".getBytes();
233         channel.write(ByteBuffer.wrap(data));
234         channel.position(0);
235         final ByteBuffer buffer = ByteBuffer.allocate(data.length);
236         final int read = channel.read(buffer);
237         assertEquals(data.length, read);
238         assertArrayEquals(data, buffer.array());
239         assertEquals(data.length, channel.position());
240     }
241 
242     @Test
243     void testReadClosed() throws IOException {
244         channel.close();
245         final ByteBuffer buffer = ByteBuffer.allocate(10);
246         assertThrows(ClosedChannelException.class, () -> channel.read(buffer));
247     }
248 
249     @Test
250     void testReadEmpty() throws IOException {
251         final ByteBuffer buffer = ByteBuffer.allocate(10);
252         final int read = channel.read(buffer);
253         assertEquals(-1, read);
254         assertEquals(0, buffer.position());
255     }
256 
257     @Test
258     void testReadNull() {
259         assertThrows(NullPointerException.class, () -> channel.read(null));
260     }
261 
262     @Test
263     void testReadSingleByte() throws IOException {
264         // Write first
265         channel.write(ByteBuffer.wrap(new byte[] { 42 }));
266         channel.position(0);
267         final ByteBuffer buffer = ByteBuffer.allocate(1);
268         final int read = channel.read(buffer);
269         assertEquals(1, read);
270         assertEquals(42, buffer.get(0));
271         assertEquals(1, channel.position());
272     }
273 
274     @Test
275     void testSizeAfterTruncateToLargerSize() throws IOException {
276         channel.write(ByteBuffer.wrap("Hello".getBytes()));
277         assertEquals(5, channel.size());
278         channel.truncate(10);
279         assertEquals(5, channel.size()); // Size should remain unchanged
280     }
281 
282     @Test
283     void testSizeAfterWrite() throws IOException {
284         assertEquals(0, channel.size());
285         channel.write(ByteBuffer.wrap("Hello".getBytes()));
286         assertEquals(5, channel.size());
287         channel.write(ByteBuffer.wrap(" World".getBytes()));
288         assertEquals(11, channel.size());
289     }
290 
291     @Test
292     void testSizeOnClosed() throws IOException {
293         channel.close();
294         assertThrows(ClosedChannelException.class, () -> channel.size());
295     }
296 
297     @Test
298     void testSizeOnNew() throws IOException {
299         assertEquals(0, channel.size());
300     }
301 
302     @Test
303     void testSizeSameOnOverwrite() throws IOException {
304         channel.write(ByteBuffer.wrap("Hello World".getBytes()));
305         assertEquals(11, channel.size());
306         channel.position(6);
307         channel.write(ByteBuffer.wrap("Test".getBytes()));
308         assertEquals(11, channel.size()); // Size should not change
309     }
310 
311     @Test
312     void testTrucateBeyondSizeReadWrite() throws IOException {
313         final ByteBuffer buffer = ByteBuffer.allocate(1);
314         channel.truncate(channel.size() + 1);
315         assertEquals(-1, channel.read(buffer));
316         channel.truncate(Integer.MAX_VALUE + 1L);
317         assertEquals(-1, channel.read(buffer));
318         assertThrows(IllegalArgumentException.class, () -> channel.truncate(-1));
319         assertThrows(IllegalArgumentException.class, () -> channel.truncate(Integer.MIN_VALUE));
320         assertThrows(IllegalArgumentException.class, () -> channel.truncate(Long.MIN_VALUE));
321     }
322 
323     @Test
324     void testTruncateNegative() {
325         assertThrows(IllegalArgumentException.class, () -> channel.truncate(-1));
326     }
327 
328     @Test
329     void testTruncateShrinks() throws IOException {
330         channel.write(ByteBuffer.wrap("Hello World".getBytes()));
331         assertEquals(11, channel.size());
332         channel.truncate(5);
333         assertEquals(5, channel.size());
334         // Position should be adjusted if it was beyond new size
335         if (channel.position() > 5) {
336             assertEquals(5, channel.position());
337         }
338     }
339 
340     @Test
341     void testWriteBeyondSizeGrows() throws IOException {
342         channel.position(100);
343         final byte[] data = "test".getBytes();
344         channel.write(ByteBuffer.wrap(data));
345         assertEquals(104, channel.size());
346         assertEquals(104, channel.position());
347         // Verify the gap contains zeros (implementation dependent)
348         channel.position(0);
349         final ByteBuffer buffer = ByteBuffer.allocate(100);
350         channel.read(buffer);
351         // Most implementations will fill gaps with zeros
352         final byte[] expectedGap = new byte[100];
353         assertArrayEquals(expectedGap, buffer.array());
354     }
355 
356     @ParameterizedTest
357     @ValueSource(ints = { 1, 10, 100, 1000, 10000 })
358     void testWriteDifferentSizes(final int size) throws IOException {
359         final byte[] data = new byte[size];
360         random.nextBytes(data);
361         final ByteBuffer buffer = ByteBuffer.wrap(data);
362         final int byteCount = channel.write(buffer);
363         assertEquals(size, byteCount);
364         assertEquals(size, channel.position());
365         assertEquals(size, channel.size());
366     }
367     @Test
368     void testWriteEmpty() throws IOException {
369         final ByteBuffer buffer = ByteBuffer.allocate(0);
370         final int byteCount = channel.write(buffer);
371         assertEquals(0, byteCount);
372         assertEquals(0, channel.position());
373         assertEquals(0, channel.size());
374     }
375     @Test
376     void testWriteNull() {
377         assertThrows(NullPointerException.class, () -> channel.write(null));
378     }
379     @Test
380     void testWritePositionReadVerify() throws IOException {
381         final byte[] originalData = "Hello, SeekableByteChannel World!".getBytes();
382         // Write data
383         channel.write(ByteBuffer.wrap(originalData));
384         // Seek to beginning
385         channel.position(0);
386         // Read data back
387         final ByteBuffer readBuffer = ByteBuffer.allocate(originalData.length);
388         final int bytesRead = channel.read(readBuffer);
389         assertEquals(originalData.length, bytesRead);
390         assertArrayEquals(originalData, readBuffer.array());
391     }
392 
393     @Test
394     void testWriteSingleByte() throws IOException {
395         final ByteBuffer buffer = ByteBuffer.allocate(1);
396         buffer.put((byte) 42);
397         buffer.flip();
398         final int written = channel.write(buffer);
399         assertEquals(1, written);
400         assertEquals(1, channel.position());
401         assertEquals(1, channel.size());
402     }
403 
404     @Test
405     void testWriteToClosedChannel() throws IOException {
406         channel.close();
407         final ByteBuffer buffer = ByteBuffer.wrap("test".getBytes());
408         assertThrows(ClosedChannelException.class, () -> channel.write(buffer));
409     }
410 
411     @Test
412     void tesWriteBytes() throws IOException {
413         final byte[] data = "Hello, World!".getBytes();
414         final ByteBuffer buffer = ByteBuffer.wrap(data);
415         final int byteCount = channel.write(buffer);
416         assertEquals(data.length, byteCount);
417         assertEquals(data.length, channel.position());
418         assertEquals(data.length, channel.size());
419     }
420 }