/* * Copyright 2014 WANdisco * * WANdisco 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 c5db.log; import c5db.interfaces.log.SequentialEntry; import c5db.interfaces.log.SequentialEntryCodec; import org.junit.Before; import org.junit.Test; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import static c5db.log.ReplicatorLogGenericTestUtil.seqNum; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.core.Is.is; public class InMemoryPersistenceNavigatorTest { private static final int MAX_SEEK = 7; private static final int LAST_SEQ_NUM = 27; private final ByteArrayPersistence persistence = new ByteArrayPersistence(); private final MethodCallCountingCodec navigatorsCodec = new MethodCallCountingCodec(); private final InMemoryPersistenceNavigator<DummyEntry> navigator = new InMemoryPersistenceNavigator<>(persistence, navigatorsCodec); private final SequentialLog<DummyEntry> log = new EncodedSequentialLog<>( persistence, new MethodCallCountingCodec(), navigator); @Before public void configureNavigatorAndPopulateTheLogWithSomeEntries() throws Exception { navigator.setMaxEntrySeek(MAX_SEEK); log.append(someConsecutiveDummyEntries(1, LAST_SEQ_NUM + 1)); } @Test public void neverDecodesAFullEntryWhileNavigating() throws Exception { performVariousNavigatorOperations(); assertThat(navigatorsCodec.numDecodes, is(0)); } @Test public void placesAnUpperBoundOnTheNumberOfEntriesItSkipsPastWhenComputingEntriesAddresses() throws Exception { for (int i = LAST_SEQ_NUM; i >= 1; i--) { final long seqNum = (long) i; int numberOfSkipOperations = numberOfSkipOperations(() -> navigator.getAddressOfEntry(seqNum)); assertThat(numberOfSkipOperations, is(lessThanOrEqualTo(MAX_SEEK))); } } @Test public void cachesAddressOfLastEntry() throws Exception { tidyGetStreamAtLastEntry(); int numberOfSkipOperationsForASecondCall = numberOfSkipOperations(() -> tidyGetStreamAtSeqNum(LAST_SEQ_NUM)); assertThat(numberOfSkipOperationsForASecondCall, is(0)); } @Test public void cachesAddressOfAPreviousEntryLookup() throws Exception { tidyGetStreamAtSeqNum(20); int numberOfSkipOperationsForASecondCall = numberOfSkipOperations(() -> tidyGetStreamAtSeqNum(20)); assertThat(numberOfSkipOperationsForASecondCall, is(0)); } @Test(expected = Exception.class) public void throwsAnExceptionIfAskedToTruncateToSeqNumZero() throws Exception { navigator.notifyTruncation(0); } @Test public void returnsAStreamPositionedAtTheFirstEntry() throws Exception { try (InputStream input = navigator.getStreamAtFirstEntry()) { assertThat(navigatorsCodec.decode(input).getSeqNum(), is(equalTo(1L))); } } @Test public void returnsAStreamPositionedAtTheLastEntry() throws Exception { try (InputStream input = navigator.getStreamAtLastEntry()) { assertThat(navigatorsCodec.decode(input).getSeqNum(), is(equalTo((long) LAST_SEQ_NUM))); } } @Test public void returnsAStreamPositionedAtASpecifiedEntry() throws Exception { long entrySeqNum = 12; try (InputStream input = navigator.getStreamAtSeqNum(entrySeqNum)) { assertThat(navigatorsCodec.decode(input).getSeqNum(), is(equalTo(entrySeqNum))); } } private void performVariousNavigatorOperations() throws Exception { tidyGetStreamAtSeqNum(20); tidyGetStreamAtSeqNum(15); tidyGetStreamAtLastEntry(); navigator.getAddressOfEntry(6); } private void tidyGetStreamAtSeqNum(long seqNum) throws Exception { navigator.getStreamAtSeqNum(seqNum).close(); } private void tidyGetStreamAtLastEntry() throws Exception { navigator.getStreamAtLastEntry().close(); } private int numberOfSkipOperations(ExceptionRunnable navigationOperation) throws Exception { int initialSkipCount = navigatorsCodec.numSkips; navigationOperation.run(); return navigatorsCodec.numSkips - initialSkipCount; } private static List<DummyEntry> someConsecutiveDummyEntries(int start, int end) { List<DummyEntry> entries = new ArrayList<>(end - start); for (int i = start; i < end; i++) { entries.add(new DummyEntry(seqNum(i))); } return entries; } private static class DummyEntry extends SequentialEntry { public DummyEntry(long seqNum) { super(seqNum); } } private class MethodCallCountingCodec implements SequentialEntryCodec<DummyEntry> { public int numDecodes = 0; public int numSkips = 0; @Override public ByteBuffer[] encode(DummyEntry entry) { ByteBuffer encoded = ByteBuffer.allocate(8).putLong(entry.getSeqNum()); encoded.flip(); return new ByteBuffer[]{encoded}; } @Override public DummyEntry decode(InputStream inputStream) throws IOException { numDecodes++; return new DummyEntry(getNextLongFrom(inputStream)); } @Override public long skipEntryAndReturnSeqNum(InputStream inputStream) throws IOException { numSkips++; return getNextLongFrom(inputStream); } private long getNextLongFrom(InputStream inputStream) throws IOException { return new DataInputStream(inputStream).readLong(); } } private interface ExceptionRunnable { public void run() throws Exception; } }