/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.cassandra.io.util;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.SyncUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.apache.cassandra.Util.expectEOF;
import static org.apache.cassandra.Util.expectException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class BufferedRandomAccessFileTest
{
@BeforeClass
public static void setupDD()
{
DatabaseDescriptor.daemonInitialization();
}
@Test
public void testReadAndWrite() throws Exception
{
SequentialWriter w = createTempFile("braf");
// writing string of data to the file
byte[] data = "Hello".getBytes();
w.write(data);
assertEquals(data.length, w.length());
assertEquals(data.length, w.position());
w.sync();
// reading small amount of data from file, this is handled by initial buffer
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath()))
{
try (FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
byte[] buffer = new byte[data.length];
assertEquals(data.length, r.read(buffer));
assertTrue(Arrays.equals(buffer, data)); // we read exactly what we wrote
assertEquals(r.read(), -1); // nothing more to read EOF
assert r.bytesRemaining() == 0 && r.isEOF();
}
// writing buffer bigger than page size, which will trigger reBuffer()
byte[] bigData = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE + 10];
for (int i = 0; i < bigData.length; i++)
bigData[i] = 'd';
long initialPosition = w.position();
w.write(bigData); // writing data
assertEquals(w.position(), initialPosition + bigData.length);
assertEquals(w.length(), initialPosition + bigData.length); // file size should equals to last position
w.sync();
// re-opening file in read-only mode
try (FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
// reading written buffer
r.seek(initialPosition); // back to initial (before write) position
data = new byte[bigData.length];
long sizeRead = 0;
for (int i = 0; i < data.length; i++)
{
data[i] = (byte) r.read();
sizeRead++;
}
assertEquals(sizeRead, data.length); // read exactly data.length bytes
assertEquals(r.getFilePointer(), initialPosition + data.length);
assertEquals(r.length(), initialPosition + bigData.length);
assertTrue(Arrays.equals(bigData, data));
assertTrue(r.bytesRemaining() == 0 && r.isEOF()); // we are at the of the file
// test readBytes(int) method
r.seek(0);
ByteBuffer fileContent = ByteBufferUtil.read(r, (int) w.length());
assertEquals(fileContent.limit(), w.length());
assert ByteBufferUtil.string(fileContent).equals("Hello" + new String(bigData));
// read the same buffer but using readFully(int)
data = new byte[bigData.length];
r.seek(initialPosition);
r.readFully(data);
assert r.bytesRemaining() == 0 && r.isEOF(); // we should be at EOF
assertTrue(Arrays.equals(bigData, data));
// try to read past mark (all methods should return -1)
data = new byte[10];
assertEquals(r.read(), -1);
assertEquals(r.read(data), -1);
assertEquals(r.read(data, 0, data.length), -1);
// test read(byte[], int, int)
r.seek(0);
data = new byte[20];
assertEquals(15, r.read(data, 0, 15));
assertTrue(new String(data).contains("Hellodddddddddd"));
for (int i = 16; i < data.length; i++)
{
assert data[i] == 0;
}
w.finish();
}
}
}
@Test
public void testReadAndWriteOnCapacity() throws IOException
{
File tmpFile = File.createTempFile("readtest", "bin");
try (SequentialWriter w = new SequentialWriter(tmpFile))
{
// Fully write the file and sync..
byte[] in = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE);
w.write(in);
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
// Read it into a same size array.
byte[] out = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE];
r.read(out);
// Cannot read any more.
int negone = r.read();
assert negone == -1 : "We read past the end of the file, should have gotten EOF -1. Instead, " + negone;
w.finish();
}
}
}
@Test
public void testLength() throws IOException
{
File tmpFile = File.createTempFile("lengthtest", "bin");
try (SequentialWriter w = new SequentialWriter(tmpFile))
{
assertEquals(0, w.length());
// write a chunk smaller then our buffer, so will not be flushed
// to disk
byte[] lessThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE / 2);
w.write(lessThenBuffer);
assertEquals(lessThenBuffer.length, w.length());
// sync the data and check length
w.sync();
assertEquals(lessThenBuffer.length, w.length());
// write more then the buffer can hold and check length
byte[] biggerThenBuffer = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE * 2);
w.write(biggerThenBuffer);
assertEquals(biggerThenBuffer.length + lessThenBuffer.length, w.length());
w.finish();
// will use cachedlength
try (FileHandle.Builder builder = new FileHandle.Builder(tmpFile.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
assertEquals(lessThenBuffer.length + biggerThenBuffer.length, r.length());
}
}
}
@Test
public void testReadBytes() throws IOException
{
final SequentialWriter w = createTempFile("brafReadBytes");
byte[] data = new byte[RandomAccessReader.DEFAULT_BUFFER_SIZE + 10];
for (int i = 0; i < data.length; i++)
{
data[i] = 'c';
}
w.write(data);
w.sync();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
ByteBuffer content = ByteBufferUtil.read(r, (int) r.length());
// after reading whole file we should be at EOF
assertEquals(0, ByteBufferUtil.compare(content, data));
assert r.bytesRemaining() == 0 && r.isEOF();
r.seek(0);
content = ByteBufferUtil.read(r, 10); // reading first 10 bytes
assertEquals(ByteBufferUtil.compare(content, "cccccccccc".getBytes()), 0);
assertEquals(r.bytesRemaining(), r.length() - content.limit());
// trying to read more than file has right now
expectEOF(() -> ByteBufferUtil.read(r, (int) r.length() + 10));
w.finish();
}
}
@Test
public void testSeek() throws Exception
{
SequentialWriter w = createTempFile("brafSeek");
byte[] data = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE + 20);
w.write(data);
w.finish();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader file = fh.createReader())
{
file.seek(0);
assertEquals(file.getFilePointer(), 0);
assertEquals(file.bytesRemaining(), file.length());
file.seek(20);
assertEquals(file.getFilePointer(), 20);
assertEquals(file.bytesRemaining(), file.length() - 20);
// trying to seek past the end of the file should produce EOFException
expectException(() -> {
file.seek(file.length() + 30);
return null;
}, IllegalArgumentException.class);
expectException(() -> {
file.seek(-1);
return null;
}, IllegalArgumentException.class); // throws IllegalArgumentException
}
}
@Test
public void testSkipBytes() throws IOException
{
SequentialWriter w = createTempFile("brafSkipBytes");
w.write(generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE * 2));
w.finish();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader file = fh.createReader())
{
file.seek(0); // back to the beginning of the file
assertEquals(file.skipBytes(10), 10);
assertEquals(file.bytesRemaining(), file.length() - 10);
int initialPosition = (int) file.getFilePointer();
// can't skip more than file size
assertEquals(file.skipBytes((int) file.length() + 10), file.length() - initialPosition);
assertEquals(file.getFilePointer(), file.length());
assert file.bytesRemaining() == 0 && file.isEOF();
file.seek(0);
// skipping negative amount should return 0
assertEquals(file.skipBytes(-1000), 0);
assertEquals(file.getFilePointer(), 0);
assertEquals(file.bytesRemaining(), file.length());
}
}
@Test
public void testGetFilePointer() throws IOException
{
final SequentialWriter w = createTempFile("brafGetFilePointer");
assertEquals(w.position(), 0); // initial position should be 0
w.write(generateByteArray(20));
assertEquals(w.position(), 20); // position 20 after writing 20 bytes
w.sync();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
// position should change after skip bytes
r.seek(0);
r.skipBytes(15);
assertEquals(r.getFilePointer(), 15);
r.read();
assertEquals(r.getFilePointer(), 16);
r.read(new byte[4]);
assertEquals(r.getFilePointer(), 20);
w.finish();
}
}
@Test
public void testGetPath() throws IOException
{
SequentialWriter file = createTempFile("brafGetPath");
assert file.getPath().contains("brafGetPath");
file.finish();
}
@Test
public void testIsEOF() throws IOException
{
for (int bufferSize : Arrays.asList(1, 2, 3, 5, 8, 64)) // smaller, equal, bigger buffer sizes
{
final byte[] target = new byte[32];
// single too-large read
for (final int offset : Arrays.asList(0, 8))
{
File file1 = writeTemporaryFile(new byte[16]);
try (FileHandle.Builder builder = new FileHandle.Builder(file1.getPath()).bufferSize(bufferSize);
FileHandle fh = builder.complete();
RandomAccessReader file = fh.createReader())
{
expectEOF(() -> { file.readFully(target, offset, 17); return null; });
}
}
// first read is ok but eventually EOFs
for (final int n : Arrays.asList(1, 2, 4, 8))
{
File file1 = writeTemporaryFile(new byte[16]);
try (FileHandle.Builder builder = new FileHandle.Builder(file1.getPath()).bufferSize(bufferSize);
FileHandle fh = builder.complete();
RandomAccessReader file = fh.createReader())
{
expectEOF(() -> {
while (true)
file.readFully(target, 0, n);
});
}
}
}
}
@Test
public void testNotEOF() throws IOException
{
try (final RandomAccessReader reader = RandomAccessReader.open(writeTemporaryFile(new byte[1])))
{
assertEquals(1, reader.read(new byte[2]));
}
}
@Test
public void testBytesRemaining() throws IOException
{
SequentialWriter w = createTempFile("brafBytesRemaining");
int toWrite = RandomAccessReader.DEFAULT_BUFFER_SIZE + 10;
w.write(generateByteArray(toWrite));
w.sync();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
assertEquals(r.bytesRemaining(), toWrite);
for (int i = 1; i <= r.length(); i++)
{
r.read();
assertEquals(r.bytesRemaining(), r.length() - i);
}
r.seek(0);
r.skipBytes(10);
assertEquals(r.bytesRemaining(), r.length() - 10);
w.finish();
}
}
@Test
public void testBytesPastMark() throws IOException
{
File tmpFile = File.createTempFile("overflowtest", "bin");
tmpFile.deleteOnExit();
// Create the BRAF by filename instead of by file.
try (FileHandle.Builder builder = new FileHandle.Builder(tmpFile.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
assert tmpFile.getPath().equals(r.getPath());
// Create a mark and move the rw there.
final DataPosition mark = r.mark();
r.reset(mark);
// Expect this call to succeed.
r.bytesPastMark(mark);
}
}
@Test
public void testClose() throws IOException
{
final SequentialWriter w = createTempFile("brafClose");
byte[] data = generateByteArray(RandomAccessReader.DEFAULT_BUFFER_SIZE + 20);
w.write(data);
w.finish();
final RandomAccessReader r = RandomAccessReader.open(new File(w.getPath()));
r.close(); // closing to test read after close
expectException(() -> r.read(), NullPointerException.class);
//Used to throw ClosedChannelException, but now that it extends BDOSP it just NPEs on the buffer
//Writing to a BufferedOutputStream that is closed generates no error
//Going to allow the NPE to throw to catch as a bug any use after close. Notably it won't throw NPE for a
//write of a 0 length, but that is kind of a corner case
expectException(() -> { w.write(generateByteArray(1)); return null; }, NullPointerException.class);
try (RandomAccessReader copy = RandomAccessReader.open(new File(r.getPath())))
{
ByteBuffer contents = ByteBufferUtil.read(copy, (int) copy.length());
assertEquals(contents.limit(), data.length);
assertEquals(ByteBufferUtil.compare(contents, data), 0);
}
}
@Test
public void testMarkAndReset() throws IOException
{
SequentialWriter w = createTempFile("brafTestMark");
w.write(new byte[30]);
w.finish();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader file = fh.createReader())
{
file.seek(10);
DataPosition mark = file.mark();
file.seek(file.length());
assertTrue(file.isEOF());
file.reset();
assertEquals(file.bytesRemaining(), 20);
file.seek(file.length());
assertTrue(file.isEOF());
file.reset(mark);
assertEquals(file.bytesRemaining(), 20);
file.seek(file.length());
assertEquals(file.bytesPastMark(), 20);
assertEquals(file.bytesPastMark(mark), 20);
file.reset(mark);
assertEquals(file.bytesPastMark(), 0);
}
}
@Test(expected = AssertionError.class)
public void testAssertionErrorWhenBytesPastMarkIsNegative() throws IOException
{
try (SequentialWriter w = createTempFile("brafAssertionErrorWhenBytesPastMarkIsNegative"))
{
w.write(new byte[30]);
w.flush();
try (FileHandle.Builder builder = new FileHandle.Builder(w.getPath());
FileHandle fh = builder.complete();
RandomAccessReader r = fh.createReader())
{
r.seek(10);
r.mark();
r.seek(0);
r.bytesPastMark();
}
}
}
@Test
public void testReadOnly() throws IOException
{
SequentialWriter file = createTempFile("brafReadOnlyTest");
byte[] data = new byte[20];
for (int i = 0; i < data.length; i++)
data[i] = 'c';
file.write(data);
file.sync(); // flushing file to the disk
// read-only copy of the file, with fixed file length
final RandomAccessReader copy = RandomAccessReader.open(new File(file.getPath()));
copy.seek(copy.length());
assertTrue(copy.bytesRemaining() == 0 && copy.isEOF());
// can't seek past the end of the file for read-only files
expectException(() -> { copy.seek(copy.length() + 1); return null; }, IllegalArgumentException.class);
copy.seek(0);
copy.skipBytes(5);
assertEquals(copy.bytesRemaining(), 15);
assertEquals(copy.getFilePointer(), 5);
assertTrue(!copy.isEOF());
copy.seek(0);
ByteBuffer contents = ByteBufferUtil.read(copy, (int) copy.length());
assertEquals(contents.limit(), copy.length());
assertTrue(ByteBufferUtil.compare(contents, data) == 0);
copy.seek(0);
int count = 0;
while (!copy.isEOF())
{
assertEquals((byte) copy.read(), 'c');
count++;
}
assertEquals(count, copy.length());
copy.seek(0);
byte[] content = new byte[10];
copy.read(content);
assertEquals(new String(content), "cccccccccc");
file.finish();
copy.close();
}
@Test (expected=IllegalArgumentException.class)
public void testSetNegativeLength() throws IOException, IllegalArgumentException
{
File tmpFile = File.createTempFile("set_negative_length", "bin");
try (SequentialWriter file = new SequentialWriter(tmpFile))
{
file.truncate(-8L);
}
}
private SequentialWriter createTempFile(String name) throws IOException
{
File tempFile = File.createTempFile(name, null);
tempFile.deleteOnExit();
return new SequentialWriter(tempFile);
}
private File writeTemporaryFile(byte[] data) throws IOException
{
File f = File.createTempFile("BRAFTestFile", null);
f.deleteOnExit();
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
SyncUtil.sync(fout);
fout.close();
return f;
}
private byte[] generateByteArray(int length)
{
byte[] arr = new byte[length];
for (int i = 0; i < length; i++)
arr[i] = 'a';
return arr;
}
}