diff --git a/src/main/java/com/iota/iri/service/snapshot/SnapshotService.java b/src/main/java/com/iota/iri/service/snapshot/SnapshotService.java index 5bb3d56aa2..5770e76254 100644 --- a/src/main/java/com/iota/iri/service/snapshot/SnapshotService.java +++ b/src/main/java/com/iota/iri/service/snapshot/SnapshotService.java @@ -1,11 +1,9 @@ package com.iota.iri.service.snapshot; -import com.iota.iri.conf.SnapshotConfig; import com.iota.iri.controllers.MilestoneViewModel; import com.iota.iri.model.Hash; import com.iota.iri.service.milestone.LatestMilestoneTracker; import com.iota.iri.service.transactionpruning.TransactionPruner; -import com.iota.iri.storage.Tangle; import java.util.Map; diff --git a/src/main/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImpl.java b/src/main/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImpl.java index d55cfdcc1a..92be47f133 100644 --- a/src/main/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImpl.java +++ b/src/main/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImpl.java @@ -1,5 +1,6 @@ package com.iota.iri.service.snapshot.impl; +import com.google.common.annotations.VisibleForTesting; import com.iota.iri.conf.SnapshotConfig; import com.iota.iri.service.milestone.LatestMilestoneTracker; import com.iota.iri.service.snapshot.LocalSnapshotManager; @@ -31,7 +32,8 @@ public class LocalSnapshotManagerImpl implements LocalSnapshotManager { * To prevent jumping back and forth in and out of sync, there is a buffer in between. * Only when the latest milestone and latest snapshot differ more than this number, we fall out of sync */ - private static final int LOCAL_SNAPSHOT_SYNC_BUFFER = 5; + @VisibleForTesting + static final int LOCAL_SNAPSHOT_SYNC_BUFFER = 5; /** * Logger for this class allowing us to dump debug and status messages. @@ -128,7 +130,8 @@ public void shutdown() { * * @param latestMilestoneTracker tracker for the milestones to determine when a new local snapshot is due */ - private void monitorThread(LatestMilestoneTracker latestMilestoneTracker) { + @VisibleForTesting + void monitorThread(LatestMilestoneTracker latestMilestoneTracker) { while (!Thread.currentThread().isInterrupted()) { int localSnapshotInterval = getSnapshotInterval(isInSync(latestMilestoneTracker)); @@ -154,38 +157,42 @@ private void monitorThread(LatestMilestoneTracker latestMilestoneTracker) { * @param inSync if this node is in sync * @return the current interval in which we take local snapshots */ - private int getSnapshotInterval(boolean inSync) { + @VisibleForTesting + int getSnapshotInterval(boolean inSync) { return inSync ? config.getLocalSnapshotsIntervalSynced() : config.getLocalSnapshotsIntervalUnsynced(); } - + /** - * A node is defined in sync when the latest snapshot milestone index and the latest milestone index are equal. - * In order to prevent a bounce between in and out of sync, a buffer is added when a node became in sync. + * A node is defined in sync when the latest snapshot milestone index and the + * latest milestone index are equal. In order to prevent a bounce between in and + * out of sync, a buffer is added when a node became in sync. * - * This will always return false if we are not done scanning milestone candidates during initialization. + * This will always return false if we are not done scanning milestone + * candidates during initialization. * * @param latestMilestoneTracker tracker we use to determine milestones * @return true if we are in sync, otherwise false */ - private boolean isInSync(LatestMilestoneTracker latestMilestoneTracker) { + @VisibleForTesting + boolean isInSync(LatestMilestoneTracker latestMilestoneTracker) { if (!latestMilestoneTracker.isInitialScanComplete()) { return false; } - + int latestIndex = latestMilestoneTracker.getLatestMilestoneIndex(); int latestSnapshot = snapshotProvider.getLatestSnapshot().getIndex(); - + // If we are out of sync, only a full sync will get us in if (!isInSync && latestIndex == latestSnapshot) { isInSync = true; - - // When we are in sync, only dropping below the buffer gets us out of sync + + // When we are in sync, only dropping below the buffer gets us out of sync } else if (latestSnapshot < latestIndex - LOCAL_SNAPSHOT_SYNC_BUFFER) { isInSync = false; } - + return isInSync; } } diff --git a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImpl.java b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImpl.java index 9114f8333b..c0b53b35ef 100644 --- a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImpl.java +++ b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImpl.java @@ -1,5 +1,6 @@ package com.iota.iri.service.snapshot.impl; +import com.google.common.annotations.VisibleForTesting; import com.iota.iri.SignedFiles; import com.iota.iri.conf.SnapshotConfig; import com.iota.iri.model.Hash; @@ -62,7 +63,8 @@ public class SnapshotProviderImpl implements SnapshotProvider { * snapshot multiple times while creating their own version of the LocalSnapshotManager, we cache the instance * here so they don't have to rebuild it from the scratch every time (massively speeds up the unit tests). */ - private static SnapshotImpl builtinSnapshot = null; + @VisibleForTesting + static SnapshotImpl builtinSnapshot = null; /** * Holds Snapshot related configuration parameters. @@ -340,7 +342,7 @@ private SnapshotState readSnapshotStatefromFile(String snapshotStateFilePath) th private SnapshotState readSnapshotStateFromJAR(String snapshotStateFilePath) throws SnapshotException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(SnapshotProviderImpl.class.getResourceAsStream(snapshotStateFilePath))))) { return readSnapshotState(reader); - } catch (IOException e) { + } catch (NullPointerException | IOException e) { throw new SnapshotException("failed to read the snapshot file from JAR at " + snapshotStateFilePath, e); } } diff --git a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotStateImpl.java b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotStateImpl.java index ef6cd3868a..3a0ba36d1a 100644 --- a/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotStateImpl.java +++ b/src/main/java/com/iota/iri/service/snapshot/impl/SnapshotStateImpl.java @@ -2,18 +2,12 @@ import com.iota.iri.controllers.TransactionViewModel; import com.iota.iri.model.Hash; -import com.iota.iri.model.HashFactory; import com.iota.iri.service.snapshot.SnapshotException; import com.iota.iri.service.snapshot.SnapshotState; import com.iota.iri.service.snapshot.SnapshotStateDiff; -import com.iota.iri.utils.IotaIOUtils; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImplTest.java new file mode 100644 index 0000000000..b1dd52bbb5 --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/LocalSnapshotManagerImplTest.java @@ -0,0 +1,149 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.any; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.exceptions.base.MockitoAssertionError; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import com.iota.iri.conf.SnapshotConfig; +import com.iota.iri.service.milestone.LatestMilestoneTracker; +import com.iota.iri.service.snapshot.SnapshotException; +import com.iota.iri.service.snapshot.SnapshotProvider; +import com.iota.iri.service.snapshot.SnapshotService; +import com.iota.iri.service.transactionpruning.TransactionPruner; +import com.iota.iri.utils.thread.ThreadUtils; + +public class LocalSnapshotManagerImplTest { + + private static final int BUFFER = LocalSnapshotManagerImpl.LOCAL_SNAPSHOT_SYNC_BUFFER; + + private static final int DELAY_SYNC = 5; + private static final int DELAY_UNSYNC = 1; + private static final int SNAPSHOT_DEPTH = 5; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private static SnapshotConfig config; + + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + SnapshotProvider snapshotProvider; + + @Mock + SnapshotService snapshotService; + + @Mock + TransactionPruner transactionPruner; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + LatestMilestoneTracker milestoneTracker; + + private LocalSnapshotManagerImpl lsManager; + + @Before + public void setUp() throws Exception { + this.lsManager = new LocalSnapshotManagerImpl(); + + lsManager.init(snapshotProvider, snapshotService, transactionPruner, config); + when(snapshotProvider.getLatestSnapshot().getIndex()).thenReturn(-5, -1, 10, 998, 999, 1999, 2000); + + when(config.getLocalSnapshotsIntervalSynced()).thenReturn(DELAY_SYNC); + when(config.getLocalSnapshotsIntervalUnsynced()).thenReturn(DELAY_UNSYNC); + when(config.getLocalSnapshotsDepth()).thenReturn(SNAPSHOT_DEPTH); + } + + @After + public void tearDown() { + lsManager.shutdown(); + } + + @Test + public synchronized void takeLocalSnapshot() throws SnapshotException { + // Always return true + when(milestoneTracker.isInitialScanComplete()).thenReturn(true); + + // When we call it, we are in sync + when(milestoneTracker.getLatestMilestoneIndex()).thenReturn(-5); + + // We are more then the depth ahead + when(snapshotProvider.getLatestSnapshot().getIndex()).thenReturn(100); + when(snapshotProvider.getInitialSnapshot().getIndex()).thenReturn(100 - SNAPSHOT_DEPTH - DELAY_SYNC - 1); + + // Run in separate thread to allow us to time-out + Thread t = new Thread(() -> lsManager.monitorThread(milestoneTracker)); + + t.start(); + // We should finish directly, margin for slower computers + ThreadUtils.sleep(100); + + // Cancel the thread + t.interrupt(); + + // Verify we took a snapshot + try { + verify(snapshotService, times(1)).takeLocalSnapshot(any(), any()); + } catch (MockitoAssertionError e) { + throw new MockitoAssertionError("A snapshot should have been taken when we are below SNAPSHOT_DEPTH"); + } + } + + @Test + public void isInSyncTestScanIncomplete() { + when(milestoneTracker.isInitialScanComplete()).thenReturn(false); + + assertFalse("We should be out of sync when he havent finished initial scans", lsManager.isInSync(milestoneTracker)); + } + + @Test + public void isInSyncTestScanComplete() { + // Always return true + when(milestoneTracker.isInitialScanComplete()).thenReturn(true); + + // We don't really support -1 indexes, but if this breaks, it is a good indication to be careful going further + when(milestoneTracker.getLatestMilestoneIndex()).thenReturn(-1, 5, 10, 998 + BUFFER - 1, 2000); + + // snapshotProvider & milestoneTracker + // -5 & -1 -> not in sync + assertFalse("Previous out of sync and not equal index should not be in sync", lsManager.isInSync(milestoneTracker)); + + // -1 and 5 -> not in sync + assertFalse("Previous out of sync and not equal index should not be in sync", lsManager.isInSync(milestoneTracker)); + + // 10 and 10 -> in sync + assertTrue("Equal index should be in sync", lsManager.isInSync(milestoneTracker)); + + // 998 and 1002 -> in sync since sync gap = 5 + assertTrue("Index difference less than the buffer still should be in sync", lsManager.isInSync(milestoneTracker)); + + // 999 and 2000 -> out of sync again, bigger gap than 5 + assertFalse("Index difference more than the buffer should be out of sync again ", lsManager.isInSync(milestoneTracker)); + + // 1999 and 2000 -> out of sync still + assertFalse("Previous out of sync and not equal index should not be in sync", lsManager.isInSync(milestoneTracker)); + + // 2000 and 2000 -> in sync again + assertTrue("Equal index should be in sync", lsManager.isInSync(milestoneTracker)); + } + + @Test + public void getDelayTest() { + assertEquals("Out of sync should return the config value at getLocalSnapshotsIntervalUnsynced", + DELAY_UNSYNC, lsManager.getSnapshotInterval(false)); + + assertEquals("In sync should return the config value at getLocalSnapshotsIntervalSynced", + DELAY_SYNC, lsManager.getSnapshotInterval(true)); + } +} diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotImplTest.java new file mode 100644 index 0000000000..2ac294fa06 --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotImplTest.java @@ -0,0 +1,68 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; + +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; + +import com.iota.iri.TransactionTestUtils; +import com.iota.iri.model.Hash; +import com.iota.iri.service.snapshot.Snapshot; +import com.iota.iri.service.snapshot.SnapshotMetaData; +import com.iota.iri.service.snapshot.SnapshotState; + +public class SnapshotImplTest { + + private static SnapshotState state; + + private static SnapshotMetaData metaData; + + @Before + public void setUp() throws Exception { + state = new SnapshotStateImpl(new HashMap<>()); + metaData = new SnapshotMetaDataImpl(Hash.NULL_HASH, 1, 1l, new HashMap<>(), new HashMap<>()); + } + + @Test + public void skippedMilestoneTest() { + Snapshot snapshot = new SnapshotImpl(state, metaData); + assertTrue("Not previously seen milestone should be accepted", snapshot.addSkippedMilestone(1)); + + assertFalse("Previously seen milestone should not be accepted", snapshot.addSkippedMilestone(1)); + assertTrue("Skipped milestone should be removed correctly", snapshot.removeSkippedMilestone(1)); + assertFalse("Not skipped milestone should fail to get removed", snapshot.removeSkippedMilestone(1)); + } + + @Test + public void updateTest() { + Snapshot snapshot = new SnapshotImpl(state, metaData); + snapshot.setIndex(0); + snapshot.setHash(Hash.NULL_HASH); + snapshot.setInitialTimestamp(1l); + + Snapshot newSnapshot = snapshot.clone(); + newSnapshot.setIndex(1); + snapshot.setHash(TransactionTestUtils.getTransactionHash()); + snapshot.setInitialTimestamp(5l); + + assertNotEquals("Modified snapshot clone should not be equal to its original", snapshot, newSnapshot); + snapshot.update(newSnapshot); + assertEquals("Updating a snapshot with another snapshot should make them equal", snapshot, newSnapshot); + } + + @Test + public void cloneTest() { + Snapshot oldSnapshot = new SnapshotImpl(state, metaData); + Snapshot newSnapshot = oldSnapshot.clone(); + + assertEquals("A clone of a snapshot is equal to its original", oldSnapshot, newSnapshot); + + oldSnapshot.addSkippedMilestone(1); + + // Clone shouldnt have the skipped milestone + assertFalse("Adding a value to a clone should be reflected on the original", newSnapshot.removeSkippedMilestone(1)); + assertNotEquals("A clone should not be equal to its original after modification", oldSnapshot, newSnapshot); + } +} diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMetaDataImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMetaDataImplTest.java new file mode 100644 index 0000000000..7e41c4696b --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMetaDataImplTest.java @@ -0,0 +1,124 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import com.iota.iri.TransactionTestUtils; +import com.iota.iri.conf.BaseIotaConfig; +import com.iota.iri.model.Hash; +import com.iota.iri.service.snapshot.SnapshotMetaData; + +public class SnapshotMetaDataImplTest { + + private static final Hash A = TransactionTestUtils.getTransactionHash(); + private static final Hash B = TransactionTestUtils.getTransactionHash(); + private static final Hash C = TransactionTestUtils.getTransactionHash(); + private static final Hash D = TransactionTestUtils.getTransactionHash(); + + private static Map solidEntryPoints = new HashMap(){{ + put(A, 1); + put(B, 2); + put(C, -1); + }}; + + private static Map seenMilestones = new HashMap(){{ + put(A, 10); + put(B, 11); + put(C, 12); + put(D, 13); + }}; + + private SnapshotMetaDataImpl meta; + + @Before + public void setUp() { + meta = new SnapshotMetaDataImpl(A, + BaseIotaConfig.Defaults.MILESTONE_START_INDEX, + BaseIotaConfig.Defaults.GLOBAL_SNAPSHOT_TIME, + solidEntryPoints, + seenMilestones); + } + + @Test + public void initialIndexTest(){ + assertEquals("Initial index should be equal to the one provided", + meta.getInitialIndex(), BaseIotaConfig.Defaults.MILESTONE_START_INDEX); + assertEquals("Current index should be equal to the initial index", + meta.getIndex(), BaseIotaConfig.Defaults.MILESTONE_START_INDEX); + + meta.setIndex(BaseIotaConfig.Defaults.MILESTONE_START_INDEX + 1); + assertNotEquals("Initial index should not be the same as current index after setting", + meta.getInitialIndex(), meta.getIndex()); + } + + @Test + public void initialTimestampTest(){ + assertEquals("Initial timestamp should be equal to the one provided", + meta.getInitialTimestamp(), BaseIotaConfig.Defaults.GLOBAL_SNAPSHOT_TIME); + assertEquals("Current timestamp should be equal to the initial timestamp", + meta.getTimestamp(), BaseIotaConfig.Defaults.GLOBAL_SNAPSHOT_TIME); + + meta.setTimestamp(BaseIotaConfig.Defaults.GLOBAL_SNAPSHOT_TIME + 1); + assertNotEquals("Initial timestamp should not be the same as current timestamp after setting", + meta.getInitialTimestamp(), meta.getTimestamp()); + } + + @Test + public void hashTest(){ + assertEquals("Initial hash should be equal to the one provided", meta.getInitialHash(), A); + + assertEquals("Current hash should be equal to the initial hash", meta.getHash(), A); + + meta.setHash(B); + assertNotEquals("Initial hash should not be the same as current hash after setting", meta.getInitialHash(), meta.getHash()); + } + + @Test + public void solidEntryPointsTest(){ + assertTrue("We should have the entry point provided on start", meta.hasSolidEntryPoint(A)); + assertTrue("We should have the entry point provided on start", meta.hasSolidEntryPoint(B)); + assertTrue("We should have the entry point provided on start", meta.hasSolidEntryPoint(C)); + + assertEquals("Index from entry should be to the one set to the hash", 1, meta.getSolidEntryPointIndex(A)); + assertEquals("Index from entry should be to the one set to the hash", 2, meta.getSolidEntryPointIndex(B)); + + // Test -1 to ensure, if we ever enforce this positive, something could break + // We don't really support -1 indexes, but if this breaks, it is a good indication to be careful going further + assertEquals("Index from entry should be to the one set to the hash", -1, meta.getSolidEntryPointIndex(C)); + + assertEquals("Solid entries amount should be the same as the ones provided", meta.getSolidEntryPoints().size(), solidEntryPoints.size()); + assertEquals("Solid entries should be the same as the ones provided", meta.getSolidEntryPoints(), new HashMap<>(solidEntryPoints)); + + meta.setSolidEntryPoints(seenMilestones); + // Right now, the map is replaced, so none are 'new' or 'existing'. + assertEquals("Existing entrypoints should have a new index", 10, meta.getSolidEntryPointIndex(A)); + assertEquals("Existing entrypoints should have a new index", 11, meta.getSolidEntryPointIndex(B)); + assertEquals("Existing entrypoints should have a new index", 12, meta.getSolidEntryPointIndex(C)); + assertEquals("New entry point added should exist and equal to the index provided", 13, meta.getSolidEntryPointIndex(D)); + } + + @Test + public void seenMilestonesTest(){ + assertEquals("Seen milestones amount should be the same as the ones provided", + meta.getSeenMilestones().size(), seenMilestones.size()); + assertEquals("Seen milestones should be the same as the ones provided", + meta.getSeenMilestones(), new HashMap<>(seenMilestones)); + } + + @Test + public void createTest(){ + SnapshotMetaData newMetaData = new SnapshotMetaDataImpl(meta); + assertEquals("new SnapshotMetaData made from another should be equal", newMetaData, meta); + + newMetaData = new SnapshotMetaDataImpl(Hash.NULL_HASH, 0, 0l, Collections.EMPTY_MAP, Collections.EMPTY_MAP); + newMetaData.update(meta); + assertEquals("Updating a SnapshotMetaData with another should make them equal", newMetaData, meta); + } + +} diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMockUtils.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMockUtils.java index 8d17c1b902..ff7e642ba7 100644 --- a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMockUtils.java +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotMockUtils.java @@ -20,20 +20,60 @@ public class SnapshotMockUtils { //region [mockSnapshotProvider] //////////////////////////////////////////////////////////////////////////////////// + /** + * Properly imitates a snapshot provider by making a real initial and latest snapshot. + * The balance of this provider is made to let the DEFAULT_GENESIS_ADDRESS (Null hash) have the entire IOTA supply. + * Genesis timestamp set to {@value #DEFAULT_GENESIS_TIMESTAMP}. + * Initial snapshot hash set to DEFAULT_GENESIS_ADDRESS. + * Starting index is {@value #DEFAULT_MILESTONE_START_INDEX} + * + * @param snapshotProvider The provider we are mocking. Must be a Mockito Mocked object + * @return The supplied snapshotProvider object + */ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotProvider) { return mockSnapshotProvider(snapshotProvider, DEFAULT_MILESTONE_START_INDEX); } + /** + * Properly imitates a snapshot provider by making a real initial and latest snapshot. + * The balance of this provider is made to let the DEFAULT_GENESIS_ADDRESS (Null hash) have the entire IOTA supply. + * Genesis timestamp set to {@value #DEFAULT_GENESIS_TIMESTAMP}. + * Initial snapshot hash set to DEFAULT_GENESIS_ADDRESS. + * + * @param snapshotProvider The provider we are mocking. Must be a Mockito Mocked object + * @param milestoneStartIndex The index we use for the genesis/initial snapshot + * @return The supplied snapshotProvider object + */ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotProvider, int milestoneStartIndex) { return mockSnapshotProvider(snapshotProvider, milestoneStartIndex, DEFAULT_GENESIS_HASH); } + /** + * Properly imitates a snapshot provider by making a real initial and latest snapshot. + * The balance of this provider is made to let the DEFAULT_GENESIS_ADDRESS (Null hash) have the entire IOTA supply. + * Genesis timestamp set to {@value #DEFAULT_GENESIS_TIMESTAMP} + * + * @param snapshotProvider The provider we are mocking. Must be a Mockito Mocked object + * @param milestoneStartIndex The index we use for the genesis/initial snapshot + * @param genesisHash The Genesis hash + * @return The supplied snapshotProvider object + */ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotProvider, int milestoneStartIndex, Hash genesisHash) { return mockSnapshotProvider(snapshotProvider, milestoneStartIndex, genesisHash, DEFAULT_GENESIS_TIMESTAMP); } + /** + * Properly imitates a snapshot provider by making a real initial and latest snapshot. + * The balance of this provider is made to let the DEFAULT_GENESIS_ADDRESS (Null hash) have the entire IOTA supply. + * + * @param snapshotProvider The provider we are mocking. Must be a Mockito Mocked object + * @param milestoneStartIndex The index we use for the genesis/initial snapshot + * @param genesisHash The Genesis hash + * @param genesisTimestamp The timestamp of the initial snapshot creation + * @return The supplied snapshotProvider object + */ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotProvider, int milestoneStartIndex, Hash genesisHash, long genesisTimestamp) { @@ -43,6 +83,16 @@ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotPro return mockSnapshotProvider(snapshotProvider, milestoneStartIndex, genesisHash, genesisTimestamp, balances); } + /** + * Properly imitates a snapshot provider by making a real initial and latest snapshot + * + * @param snapshotProvider The provider we are mocking. Must be a Mockito Mocked object + * @param milestoneStartIndex The index we use for the genesis/initial snapshot + * @param genesisHash The Genesis hash + * @param genesisTimestamp The timestamp of the initial snapshot creation + * @param balances The balances to add to the provider + * @return The supplied snapshotProvider object + */ public static SnapshotProvider mockSnapshotProvider(SnapshotProvider snapshotProvider, int milestoneStartIndex, Hash genesisHash, long genesisTimestamp, Map balances) { diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImplTest.java new file mode 100644 index 0000000000..8e51f88648 --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotProviderImplTest.java @@ -0,0 +1,59 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import com.iota.iri.conf.ConfigFactory; +import com.iota.iri.conf.IotaConfig; +import com.iota.iri.model.Hash; +import com.iota.iri.service.snapshot.SnapshotException; +import com.iota.iri.service.spentaddresses.SpentAddressesException; + +public class SnapshotProviderImplTest { + + private SnapshotProviderImpl provider; + + private SnapshotImpl cachedBuildinSnapshot; + + @Before + public void setUp(){ + provider = new SnapshotProviderImpl(); + + // When running multiple tests, the static cached snapshot breaks this test + cachedBuildinSnapshot = SnapshotProviderImpl.builtinSnapshot; + SnapshotProviderImpl.builtinSnapshot = null; + } + + @After + public void tearDown(){ + provider.shutdown(); + + // Set back the cached snapshot for tests after us who might use it + SnapshotProviderImpl.builtinSnapshot = cachedBuildinSnapshot; + } + + @Test + public void testGetLatestSnapshot() throws SnapshotException, SpentAddressesException { + IotaConfig iotaConfig = ConfigFactory.createIotaConfig(true); + provider.init(iotaConfig); + + // If we run this on its own, it correctly takes the testnet milestone + // However, running it with all tests makes it load the last global snapshot contained in the jar + assertEquals("Initial snapshot index should be the same as the milestone start index", + iotaConfig.getMilestoneStartIndex(), provider.getInitialSnapshot().getIndex()); + + assertEquals("Initial snapshot timestamp should be the same as last snapshot time", + iotaConfig.getSnapshotTime(), provider.getInitialSnapshot().getInitialTimestamp()); + + assertEquals("Initial snapshot hash should be the genisis transaction", + Hash.NULL_HASH, provider.getInitialSnapshot().getHash()); + + assertEquals("Initial provider snapshot should be equal to the latest snapshot", + provider.getInitialSnapshot(), provider.getLatestSnapshot()); + + assertTrue("Initial snapshot should have a filled map of addresses", provider.getInitialSnapshot().getBalances().size() > 0); + assertTrue("Initial snapshot supply should be equal to all supply", provider.getInitialSnapshot().hasCorrectSupply()); + } +} diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateDiffImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateDiffImplTest.java new file mode 100644 index 0000000000..6b2726b99e --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateDiffImplTest.java @@ -0,0 +1,49 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import com.iota.iri.TransactionTestUtils; +import com.iota.iri.model.Hash; + +public class SnapshotStateDiffImplTest { + + private static final Hash A = TransactionTestUtils.getTransactionHash(); + private static final Hash B = TransactionTestUtils.getTransactionHash(); + private static final Hash C = TransactionTestUtils.getTransactionHash(); + + @Test + public void getBalanceChanges() { + SnapshotStateDiffImpl stateDiff = new SnapshotStateDiffImpl(Collections.EMPTY_MAP); + Map change = stateDiff.getBalanceChanges(); + change.put(A, 1l); + + assertNotEquals("Changes to the statediff balance changes shouldnt reflect on the original state", + stateDiff.getBalanceChanges().size(), change.size()); + } + + @Test + public void isConsistent() { + SnapshotStateDiffImpl stateDiff = new SnapshotStateDiffImpl(new HashMap(){{ + put(A, 1l); + put(B, 5l); + put(C, -6l); + }}); + assertTrue("Sum of diffs should be 0", stateDiff.isConsistent()); + + stateDiff = new SnapshotStateDiffImpl(Collections.EMPTY_MAP); + assertTrue("Empty diff should be consisntent as sum is 0", stateDiff.isConsistent()); + + stateDiff = new SnapshotStateDiffImpl(new HashMap(){{ + put(A, 1l); + put(B, 5l); + }}); + + assertFalse("Diff sum not 0 shouldnt be consistent", stateDiff.isConsistent()); + } +} diff --git a/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateImplTest.java b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateImplTest.java new file mode 100644 index 0000000000..83f5bef544 --- /dev/null +++ b/src/test/java/com/iota/iri/service/snapshot/impl/SnapshotStateImplTest.java @@ -0,0 +1,135 @@ +package com.iota.iri.service.snapshot.impl; + +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import com.iota.iri.TransactionTestUtils; +import com.iota.iri.controllers.TransactionViewModel; +import com.iota.iri.model.Hash; +import com.iota.iri.service.snapshot.SnapshotException; +import com.iota.iri.service.snapshot.SnapshotState; +import com.iota.iri.service.snapshot.SnapshotStateDiff; + +public class SnapshotStateImplTest { + + private static final Hash A = TransactionTestUtils.getTransactionHash(); + private static final Hash B = TransactionTestUtils.getTransactionHash(); + + private static Map map = new HashMap(){{ + put(Hash.NULL_HASH, TransactionViewModel.SUPPLY - 10); + put(A, 10l); + }}; + + private static Map inconsistentMap = new HashMap(){{ + put(Hash.NULL_HASH, 5l); + put(A, -10l); + }}; + + private SnapshotStateImpl state; + private SnapshotStateImpl balanceState; + + @Before + public void setUp() throws Exception { + state = new SnapshotStateImpl(new HashMap<>()); + balanceState = new SnapshotStateImpl(map); + } + + @Test + public void testGetBalance() { + assertNull("Unknown address should return null", balanceState.getBalance(null)); + + long balance = balanceState.getBalance(Hash.NULL_HASH); + assertEquals("Balance should be total - 10", TransactionViewModel.SUPPLY - 10l, balance); + + balance = balanceState.getBalance(A); + assertEquals("Balance should be 10", 10l, balance); + } + + @Test + public void testGetBalances() { + assertEquals("State should not have balances", new HashMap<>(), state.getBalances()); + assertEquals("State should have the balances it was created with", map, balanceState.getBalances()); + } + + @Test + public void testIsConsistent() { + assertTrue("Empty balance should be consistent", state.isConsistent()); + assertTrue("No negative balances should be consistent", balanceState.isConsistent()); + + SnapshotStateImpl inconsistentState = new SnapshotStateImpl(inconsistentMap); + assertFalse("Negative balances should not be consistent", inconsistentState.isConsistent()); + } + + @Test + public void testHasCorrectSupply() { + assertFalse("Empty state should not have correct supply", state.hasCorrectSupply()); + assertTrue("State with total supply should have correct supply", balanceState.hasCorrectSupply()); + + SnapshotStateImpl inconsistentState = new SnapshotStateImpl(inconsistentMap); + assertFalse("Inconsistent state without full supply should be incorrect", inconsistentState.hasCorrectSupply()); + + Map map = new HashMap<>(); + map.put(Hash.NULL_HASH, TransactionViewModel.SUPPLY - 10); + map.put(A, -10l); + map.put(B, 20l); + assertFalse("Inconsistent state with full supply should be correct", inconsistentState.hasCorrectSupply()); + } + + @Test + public void testUpdate() { + assertNotEquals("States with different balances should not be equal", state, balanceState); + state.update(balanceState); + + assertEquals("Updating a state with another state should make them equal", state, balanceState); + } + + @Test + public void testApplyStateDiff() throws SnapshotException { + Map map = new HashMap<>(); + map.put(Hash.NULL_HASH, 5l); + map.put(A, -5l); + + SnapshotStateDiff diff = new SnapshotStateDiffImpl(map); + state.applyStateDiff(diff); + + long balance = state.getBalance(Hash.NULL_HASH); + assertEquals("Applying state to an empty state should have 5 for genesis", 5l, balance); + + balance = state.getBalance(A); + assertEquals("Applying state to an empty state should have -5 for A", -5l, balance); + } + + @Test(expected = SnapshotException.class) + public void testApplyStateDiffThrowsException() throws SnapshotException { + SnapshotStateDiff diff = new SnapshotStateDiffImpl(inconsistentMap); + state.applyStateDiff(diff); + + fail("Applying an inconsistent state should throw an exception"); + } + + @Test + public void testPatchedState() { + SnapshotStateDiff diff = new SnapshotStateDiffImpl(map); + SnapshotState patchedState = state.patchedState(diff); + + assertEquals("Patching an empty state with a map should equal to creation with that map", patchedState, balanceState); + + Map map = new HashMap<>(); + map.put(Hash.NULL_HASH, 5l); + map.put(A, -5l); + + diff = new SnapshotStateDiffImpl(map); + patchedState = balanceState.patchedState(diff); + + long balance = patchedState.getBalance(Hash.NULL_HASH); + assertEquals("5 should have been added to genesis", TransactionViewModel.SUPPLY - 5l, balance); + + balance = patchedState.getBalance(A); + assertEquals("5 should have been removed from A", 5, balance); + } +}