package io.eguan.dtx.journal; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * Licensed 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. * #L% */ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import io.eguan.dtx.DtxTestHelper; import io.eguan.dtx.journal.JournalRecord; import io.eguan.proto.Common.ProtocolVersion; import io.eguan.proto.dtx.DistTxWrapper; import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.ReadableByteChannel; import java.util.Arrays; import org.junit.Test; /** * Tests for the {@link JournalRecord} class. * * @author oodrive * @author pwehrle * */ public final class TestJournalRecord { // long and integer sizes in bytes private static final int LONG_SIZE_BYTES = Long.SIZE / Byte.SIZE; private static final int INT_SIZE_BYTES = Integer.SIZE / Byte.SIZE; /** * This timestamp is chosen explicitly as it makes errors reproducible (e.g. its protobuf encoding contains -1, so * reading it through an InputStream fails). */ private static final long DEFAULT_TX_TIMESTAMP = 1365669478398L; /** * Dummy implementation of the {@link ReadableByteChannel} class for testing purposes. * * */ private static class DummyReadableByteChannel implements ReadableByteChannel { private boolean open = true; private final ByteBuffer innerBuffer; public DummyReadableByteChannel(final byte[] content) { this.innerBuffer = ByteBuffer.wrap(content); } @Override public final boolean isOpen() { return this.open; } @Override public final void close() throws IOException { this.open = false; } @Override public final int read(final ByteBuffer dst) throws IOException { if (!isOpen()) { throw new ClosedChannelException(); } final byte[] data = new byte[Math.min(dst.remaining(), innerBuffer.remaining())]; innerBuffer.get(data); dst.put(data); return data.length; } } /** * Tests the construction of a {@link JournalRecord} from a given entry and compares to the result produced by * {@link JournalRecord#buildJournalRecord(byte[])}. */ @Test public final void testNewJournalRecord() { final long txId = DtxTestHelper.nextTxId(); final byte[] entry1 = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(txId).setOp(TxOpCode.COMMIT).build().toByteArray(); final byte[] entry2 = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(txId).setOp(TxOpCode.ROLLBACK).build().toByteArray(); final JournalRecord target1 = new JournalRecord(entry1); assertNotNull(target1); assertTrue(Arrays.equals(entry1, target1.getEntry())); assertFalse(entry1 == target1.getEntry()); final JournalRecord target2 = new JournalRecord(entry2); assertNotNull(target2); assertTrue(Arrays.equals(entry2, target2.getEntry())); assertFalse(entry2 == target2.getEntry()); assertFalse(target1.getChecksum() == target2.getChecksum()); final JournalRecord builtRecord1 = JournalRecord.buildJournalRecord(target1.getContent()); assertTrue(Arrays.equals(target1.getContent(), builtRecord1.getContent())); assertEquals(target1.getChecksum(), builtRecord1.getChecksum()); final JournalRecord builtRecord2 = JournalRecord.buildJournalRecord(target2.getContent()); assertTrue(Arrays.equals(target2.getContent(), builtRecord2.getContent())); assertEquals(target2.getChecksum(), builtRecord2.getChecksum()); } /** * Tests failure to construct a {@link JournalRecord} from an empty entry. */ @Test(expected = IllegalArgumentException.class) public final void testCreateRecordFailMissingEntry() { new JournalRecord(new byte[0]); } /** * Tests failure to construct a {@link JournalRecord} from a <code>null</code> entry. */ @Test(expected = NullPointerException.class) public final void testCreateRecordFailNullEntry() { new JournalRecord(null); } /** * Tests the {@link JournalRecord#buildJournalRecord(byte[])} method's failure due to insufficient entry content. * * @throws IOException * not part of this test * @throws IllegalArgumentException * if the argument is invalid, expected for this test */ @Test(expected = IllegalArgumentException.class) public final void testBuildJournalRecordFailContentTooShort() throws IOException, IllegalArgumentException { final byte[] entry = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(DtxTestHelper.nextTxId()).setOp(TxOpCode.ROLLBACK).build().toByteArray(); final byte[] recordContent = new JournalRecord(entry).getContent(); final byte[] tooShortContent = Arrays.copyOf(recordContent, recordContent.length - 2); JournalRecord.buildJournalRecord(tooShortContent); } /** * Tests the {@link JournalRecord#buildJournalRecord(byte[])} method's failure due to content too short to provide * the length field. * * @throws IOException * not part of this test * @throws IllegalArgumentException * if the argument is invalid, expected for this test */ @Test(expected = IllegalArgumentException.class) public final void testBuildJournalRecordFailContentMuchTooShort() throws IOException, IllegalArgumentException { final byte[] entry = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(DtxTestHelper.nextTxId()).setOp(TxOpCode.ROLLBACK).build().toByteArray(); final byte[] recordContent = new JournalRecord(entry).getContent(); final byte[] muchTooShortContent = Arrays.copyOf(recordContent, INT_SIZE_BYTES + 1); JournalRecord.buildJournalRecord(muchTooShortContent); } /** * Tests the {@link JournalRecord#buildJournalRecord(byte[])} method's failure due to a bad checksum. * * @throws IOException * not part of this test * @throws IllegalArgumentException * if the checksum is invalid, expected for this test */ @Test(expected = IllegalArgumentException.class) public final void testBuildJournalRecordFailBadChecksum() throws IOException, IllegalArgumentException { final byte[] entry = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(DtxTestHelper.nextTxId()).setOp(TxOpCode.ROLLBACK).build().toByteArray(); final byte[] recordContent = new JournalRecord(entry).getContent(); final byte[] badChksmContent = Arrays.copyOf(recordContent, recordContent.length); ByteBuffer.wrap(badChksmContent).putLong(badChksmContent.length - LONG_SIZE_BYTES, -1); JournalRecord.buildJournalRecord(badChksmContent); } /** * Tests the {@link JournalRecord#readRecordFromByteChannel(ReadableByteChannel)} method. * * @throws IOException * if reading from the input fails, not part of this test */ @Test public final void testReadRecordFromByteChannel() throws IOException { final byte[] entry = DistTxWrapper.TxJournalEntry.newBuilder().setTimestamp(DEFAULT_TX_TIMESTAMP) .setVersion(ProtocolVersion.VERSION_1).setTxId(DtxTestHelper.nextTxId()).setOp(TxOpCode.COMMIT).build().toByteArray(); final JournalRecord record = new JournalRecord(entry); final ReadableByteChannel testChannel = new DummyReadableByteChannel(record.getContent()); final JournalRecord result = JournalRecord.readRecordFromByteChannel(testChannel); assertTrue(Arrays.equals(record.getContent(), result.getContent())); assertEquals(record.getChecksum(), result.getChecksum()); assertTrue(Arrays.equals(record.getEntry(), result.getEntry())); } /** * Tests the {@link JournalRecord#readRecordFromByteChannel(ReadableByteChannel)} method's failure due to a lack of * data in provided by the channel (no exception thrown). * * @throws IOException * if reading from the input fails, not part of this test */ @Test public final void testReadRecordFromByteChannelFailNotEnoughData() throws IOException { final byte[] badContent = new byte[2 * INT_SIZE_BYTES]; Arrays.fill(badContent, (byte) 1); // channel only contains two integers final JournalRecord shortResult = JournalRecord.readRecordFromByteChannel(new DummyReadableByteChannel( badContent)); assertTrue(shortResult == null); final byte[] worseContent = new byte[INT_SIZE_BYTES / 2]; Arrays.fill(worseContent, (byte) 2); // channel reads even less than an integer final JournalRecord shorterResult = JournalRecord.readRecordFromByteChannel(new DummyReadableByteChannel( worseContent)); assertTrue(shorterResult == null); } /** * Tests the {@link JournalRecord#readRecordFromByteChannel(ReadableByteChannel)} method's failure due to a * <code>null</code> argument. * * @throws IOException * if reading from the input fails, not part of this test * @throws NullPointerException * expected for this test */ @Test(expected = NullPointerException.class) public final void testReadRecordFromByteChannelFailNullChannel() throws IOException, NullPointerException { JournalRecord.readRecordFromByteChannel(null); } /** * Tests the {@link JournalRecord#readRecordFromByteChannel(ReadableByteChannel)} method's failure due to a zero in * the length field. * * @throws IOException * if reading from the input fails, not part of this test * @throws IllegalArgumentException * if the length is invalid, expected for this test */ @Test(expected = IllegalArgumentException.class) public final void testReadRecordFromByteChannelFailLengthZero() throws IOException, IllegalArgumentException { final byte[] emptyEntryContent = new byte[INT_SIZE_BYTES + LONG_SIZE_BYTES]; final IntBuffer entryBuffer = ByteBuffer.wrap(emptyEntryContent).asIntBuffer(); entryBuffer.put(0); // channel contains an entry of zero length JournalRecord.readRecordFromByteChannel(new DummyReadableByteChannel(emptyEntryContent)); } /** * Tests the {@link JournalRecord#readRecordFromByteChannel(ReadableByteChannel)} method's failure due to a negative * value in the length field. * * @throws IOException * if reading from the input fails, not part of this test * @throws IllegalArgumentException * if the length is invalid, expected for this test */ @Test(expected = IllegalArgumentException.class) public final void testReadRecordFromByteChannelFailLengthNegative() throws IOException, IllegalArgumentException { final byte[] emptyEntryContent = new byte[INT_SIZE_BYTES + LONG_SIZE_BYTES]; final IntBuffer entryBuffer = ByteBuffer.wrap(emptyEntryContent).asIntBuffer(); entryBuffer.put(-1); // channel contains an entry of zero length JournalRecord.readRecordFromByteChannel(new DummyReadableByteChannel(emptyEntryContent)); } }