package bitronix.tm.journal.nio; import org.junit.Before; import org.junit.Test; import javax.transaction.Status; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashSet; import java.util.Random; import java.util.UUID; import static bitronix.tm.journal.nio.NioJournalConstants.NAME_CHARSET; import static bitronix.tm.journal.nio.NioJournalFileRecord.*; import static bitronix.tm.journal.nio.NioJournalFileRecord.ReadStatus.*; import static bitronix.tm.utils.UidGenerator.generateUid; import static java.lang.System.arraycopy; import static java.nio.ByteBuffer.allocate; import static java.nio.ByteBuffer.wrap; import static java.util.Arrays.asList; import static org.junit.Assert.*; /** * Implements functionality tests on the class NioJournalFileRecord. * * @author juergen kellerer, 2011-08-28 */ public class NioJournalFileRecordTest { static final UUID expectedDelimiter = UUID.randomUUID(), otherDelimiter = UUID.randomUUID(); static Random random = new Random(); NioJournalRecord payload = new NioJournalRecord(Status.STATUS_ACTIVE, generateUid(), new HashSet<String>(asList("a name"))); byte[] payloadBytes; NioJournalFileRecord fileRecord; @Before public void setUp() throws Exception { payloadBytes = new byte[payload.getRecordLength()]; payload.encodeTo(wrap(payloadBytes), false); fileRecord = new NioJournalFileRecord(expectedDelimiter); } private byte[] recordToBytes(NioJournalFileRecord fileRecord) { byte[] buffer = new byte[fileRecord.getRecordSize()]; ByteBuffer wrap = wrap(buffer); fileRecord.writeRecord(expectedDelimiter, wrap); assertFalse("getRecordSize() returned an invalid size", wrap.hasRemaining()); return buffer; } private byte[] recordToBytes() throws Exception { testCanCreateEmptyPayloadAndWriteToIt(); return recordToBytes(fileRecord); } private void createNoise(byte[] chunk) { random.nextBytes(chunk); // Remove values that could randomly lead to the detection of partial records. for (int c = chunk.length - 1; c >= 0; c--) if (chunk[c] == '[') chunk[c] = ' '; Arrays.fill(chunk, chunk.length - 4, chunk.length, (byte) ' '); } @Test public void testCanCreateEmptyPayloadAndWriteToIt() throws Exception { fileRecord.createEmptyPayload(payloadBytes.length).put(payloadBytes); assertEquals(wrap(payloadBytes), fileRecord.getPayload()); } @Test public void testCanWriteAndReadRecord() throws Exception { fileRecord = NioJournalFileRecord.readRecord(expectedDelimiter, wrap(recordToBytes())); assertTrue("is not valid", fileRecord.isValid()); assertEquals(expectedDelimiter, fileRecord.getDelimiter()); assertEquals(wrap(payloadBytes), fileRecord.getPayload()); } @Test public void testCanReadInvalidEntry() throws Exception { byte[] recordBytes = recordToBytes(); int namePosition = new String(recordBytes, NAME_CHARSET.name()).indexOf("a name"); ByteBuffer buffer = wrap(recordBytes); buffer.put(namePosition, (byte) '#'); fileRecord = NioJournalFileRecord.readRecord(expectedDelimiter, buffer); assertFalse("is valid", fileRecord.isValid()); assertEquals("# name", new NioJournalRecord(fileRecord.getPayload(), false).getUniqueNames().iterator().next()); } @Test(expected = IllegalArgumentException.class) public void testCannotExceedRecordMaxSize() throws Exception { fileRecord.createEmptyPayload(JOURNAL_MAX_RECORD_SIZE + 1); } @Test(expected = IllegalArgumentException.class) public void testCannotCreateNegativeSizedPayload() throws Exception { fileRecord.createEmptyPayload(-1); } @Test(expected = IllegalStateException.class) public void testCannotWriteEmptyRecord() throws Exception { fileRecord.writeRecord(expectedDelimiter, allocate(fileRecord.getRecordSize())); } @Test public void testCannotWriteDisposedRecord() throws Exception { testCanCreateEmptyPayloadAndWriteToIt(); fileRecord.writeRecord(expectedDelimiter, allocate(fileRecord.getRecordSize())); fileRecord.dispose(); try { fileRecord.writeRecord(expectedDelimiter, allocate(fileRecord.getRecordSize())); fail("expected IllegalStateException"); } catch (IllegalStateException e) { } } @Test public void testSkipsOtherValidRecords() throws Exception { //To change body of created methods use File | Settings | File Templates. } @Test public void testDoesNotSkipOtherInvalidRecords() throws Exception { //To change body of created methods use File | Settings | File Templates. } @Test public void testFindsRecordAtArbitraryPositionInBuffer() throws Exception { final byte[] chunk = new byte[8 * 1024]; final byte[] recordBytes = recordToBytes(); final ByteBuffer byteBuffer = wrap(chunk); final int rightMargin = chunk.length - recordBytes.length; for (int i = 0; i <= rightMargin; i++) { createNoise(chunk); arraycopy(recordBytes, 0, chunk, i, recordBytes.length); byteBuffer.clear(); NioJournalFileRecord.FindResult result = findNextRecord(expectedDelimiter, byteBuffer); assertEquals(ReadOk, result.getStatus()); assertEquals(recordBytes.length, result.getRecord().getRecordSize()); assertEquals("didn't find correct record position.", i, byteBuffer.position() - recordBytes.length); assertEquals(chunk.length - i - recordBytes.length, byteBuffer.remaining()); } } @Test public void testFindsForwardLookingPartialRecords() throws Exception { final byte[] chunk = new byte[8 * 1024]; final byte[] recordBytes = recordToBytes(); final ByteBuffer byteBuffer = wrap(chunk); for (int i = (chunk.length - recordBytes.length) + 1; i < chunk.length; i++) { createNoise(chunk); arraycopy(recordBytes, 0, chunk, i, chunk.length - i); byteBuffer.clear(); NioJournalFileRecord.FindResult result = findNextRecord(expectedDelimiter, byteBuffer); assertEquals(FoundPartialRecord, result.getStatus()); assertEquals("didn't find correct partial record position.", i, byteBuffer.position()); } } @Test public void testFindsBrokenRecordsIfHeaderAndTrailerAreIntact() throws Exception { final byte[] chunk = new byte[4 * 1024]; final byte[] recordBytes = recordToBytes(); final ByteBuffer byteBuffer = wrap(chunk); byte corruptionByte = '#'; String recordString = new String(recordBytes, NAME_CHARSET.name()); while (recordString.indexOf((char) corruptionByte) != -1) corruptionByte = (byte) random.nextInt(256); for (int repetitions = 10; repetitions > 0; repetitions--) for (int i = 1000; i < 2000; i++) { createNoise(chunk); //Arrays.fill(chunk, (byte) 0x0); arraycopy(recordBytes, 0, chunk, i, recordBytes.length); byteBuffer.clear(); int corruption = 1500; byteBuffer.put(corruption, corruptionByte); FindResult findResult = findNextRecord(expectedDelimiter, byteBuffer); boolean recordIsOutsideOfCorruption = i > corruption || i + recordBytes.length <= corruption; if (recordIsOutsideOfCorruption) { assertTrue("didn't find valid record at pos " + i + " (" + findResult + ")", findResult.getRecord() != null && findResult.getRecord().isValid()); } else { boolean corruptionInsidePayload = corruption >= i + RECORD_HEADER_SIZE && corruption < i + recordBytes.length - RECORD_TRAILER_SIZE; boolean corruptionInsideCRC32 = corruption >= i + RECORD_CRC32_OFFSET && corruption < i + RECORD_CRC32_OFFSET + 4; boolean corruptionInsideLengthField = corruption >= i + RECORD_CRC32_OFFSET - 4 && corruption < i + RECORD_CRC32_OFFSET; if (corruptionInsidePayload || corruptionInsideCRC32) { assertFalse("didn't find corrupted record at pos " + i + " (" + findResult + ")", findResult.getRecord() == null || findResult.getRecord().isValid()); } else if (corruptionInsideLengthField) { assertTrue("pos " + i + " (" + findResult + ")", asList(NoHeaderInBuffer, FoundPartialRecord).contains(findResult.getStatus())); } else { assertEquals("pos " + i + " (" + findResult + ")", NoHeaderInBuffer, findResult.getStatus()); } } } } }