/******************************************************************************* * Copyright (c) 2017 École Polytechnique de Montréal * * All rights reserved. This program and the accompanying materials are * made available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.tracecompass.internal.provisional.datastore.core.historytree; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; import java.util.List; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.AbstractHistoryTree; import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.HTNode; import org.eclipse.tracecompass.internal.provisional.datastore.core.interval.IHTInterval; import org.junit.After; import org.junit.Before; import org.junit.Test; /** * This is the base class for all history tree implementation tests. Specific * tree's tests can extend this class to have some basic tests. * * It tests the {@link AbstractHistoryTree} class. This test class will fill * nodes only with sequential objects, so that each extending test for trees * will have to add the tests and filling methods that correspond to their own * use cases. * * @author Geneviève Bastien * @param <E> * The type of objects that will be saved in the tree * @param <N> * The base type of the nodes of this tree */ public abstract class AbstractHistoryTreeTestBase<E extends IHTInterval, N extends HTNode<E>> { private @Nullable File fTempFile; /** * Create the temporary file for this history tree */ @Before public void setupTest() { try { fTempFile = File.createTempFile("tmpStateSystem", null); } catch (IOException e) { fail(e.getMessage()); } } /** * Delete the temporary history tree file after the test */ @After public void cleanup() { if (fTempFile != null) { fTempFile.delete(); } } /** * Setup a history tree. * * @param maxChildren * The max number of children per node in the tree (tree config * option) * @param treeStart * The start of the tree * @return The new history tree */ protected AbstractHistoryTree<E, N> setupSmallTree(int maxChildren, long treeStart) { AbstractHistoryTree<E, N> ht = null; try { File newFile = fTempFile; assertNotNull(newFile); ht = createHistoryTree(newFile, HtTestUtils.BLOCKSIZE, maxChildren, /* Number of children */ 1, /* Provider version */ 1); /* Start time */ } catch (IOException e) { fail(e.getMessage()); } assertNotNull(ht); return ht; } /** * Create the history tree to test * * @param stateHistoryFile * The name of the history file * @param blockSize * The size of each "block" on disk in bytes. One node will * always fit in one block. It should be at least 4096. * @param maxChildren * The maximum number of children allowed per core (non-leaf) * node. * @param providerVersion * The version of the state provider. If a file already exists, * and their versions match, the history file will not be rebuilt * uselessly. * @param treeStart * The start time of the history * @return The history tree stub * @throws IOException * Any exception thrown by the history tree */ protected abstract AbstractHistoryTree<E, N> createHistoryTree(File stateHistoryFile, int blockSize, int maxChildren, int providerVersion, long treeStart) throws IOException; /** * "Reader" constructor : instantiate a history tree from an existing tree * file on disk * * @param existingStateFile * Path/filename of the history-file we are to open * @param expProviderVersion * The expected version of the state provider * @return The history tree stub * @throws IOException * If an error happens reading the file */ protected abstract AbstractHistoryTree<E, N> createHistoryTree( File existingStateFile, int expProviderVersion) throws IOException; /** * Create an interval that fits in the tree with the given start/end time * * @param start * The start time * @param end * The end time * @return The object */ protected abstract E createInterval(long start, long end); /** * Create a reader history tree * * @return The history tree stub * @throws IOException * If an error happened reading the file */ protected final AbstractHistoryTree<E, N> createHistoryTreeReader() throws IOException { File tempFile = fTempFile; assertNotNull(tempFile); return createHistoryTree(tempFile, 1); } /** * Setup a history tree with config MAX_CHILDREN = 3 and start time of 1 * * @return A new history tree */ protected AbstractHistoryTree<E, N> setupSmallTree() { return setupSmallTree(3, 1); } /** * Add sequential elements to the history up to a certain size. Any element * that would go above the fixed limit should not be added * * @param ht * The tree to which to add values * @param fillSize * The limit on the size of the elements to add * @param start * The start time of the values * @return The latest end time */ protected abstract long fillValues(AbstractHistoryTree<E, N> ht, int fillSize, long start); /** * Insert objects in the tree to fill the current leaf node to capacity, * without exceeding it. * * This guarantees that the following insertion will create new nodes. * * @param ht * The history tree in which to insert * @return Start time of the current leaf node. Future insertions should be * greater than or equal to this to make sure the intervals go in * the leaf node. */ private long fillNextLeafNode(AbstractHistoryTree<E, N> ht, long leafNodeStart) { int prevCount = ht.getNodeCount(); int prevDepth = ht.getDepth(); /* Fill the following leaf node */ N node = ht.getLatestLeaf(); long ret = fillValues(ht, node.getNodeFreeSpace(), leafNodeStart); /* Make sure we haven't changed the depth or node count */ assertEquals(prevCount, ht.getNodeCount()); assertEquals(prevDepth, ht.getDepth()); return ret; } /** * Test that nodes are filled * * It fills nodes with sequential elements, so that leafs should be filled. */ @Test public void testSequentialFill() { AbstractHistoryTree<E, N> ht = setupSmallTree(); // Make sure it is empty N node = ht.getLatestLeaf(); assertEquals(0, node.getNodeUsagePercent()); /* Fill ~10% of the node with elements */ int initialFreeSpace = node.getNodeFreeSpace(); int limit = node.getNodeFreeSpace() / 10; long start = fillValues(ht, limit, 1); assertTrue(node.getNodeFreeSpace() > initialFreeSpace - limit); assertTrue(node.getNodeFreeSpace() < initialFreeSpace); /* Add elements up to ~20% */ start = fillValues(ht, limit, start); assertTrue(node.getNodeFreeSpace() > initialFreeSpace - 2 * limit); assertTrue(node.getNodeFreeSpace() < initialFreeSpace - limit); /* Add elements up to ~30% */ start = fillValues(ht, limit, start); assertTrue(node.getNodeFreeSpace() > initialFreeSpace - 3 * limit); assertTrue(node.getNodeFreeSpace() < initialFreeSpace - 2 * limit); /* Add elements up to ~40% */ start = fillValues(ht, limit, start); assertTrue(node.getNodeFreeSpace() > initialFreeSpace - 4 * limit); assertTrue(node.getNodeFreeSpace() < initialFreeSpace - 3 * limit); // Assert the integrity of the tree ht.closeTree(ht.getTreeEnd()); HtTestUtils.assertTreeIntegrity(ht); } /** * Test the addition of new nodes to the tree and make sure the tree is * built with the right structure */ @Test public void testDepth() { AbstractHistoryTree<E, N> ht = setupSmallTree(); /* Fill a first node */ N node = ht.getLatestLeaf(); long start = 1; long time = fillValues(ht, node.getNodeFreeSpace(), start); /* * Add intervals that should add a sibling to the node and a new root * node */ assertEquals(1, ht.getNodeCount()); assertEquals(1, ht.getDepth()); ht.insert(createInterval(time, time + 1)); time += 1; assertEquals(3, ht.getNodeCount()); assertEquals(2, ht.getDepth()); /* Fill the latest leaf node (2nd child) */ node = ht.getLatestLeaf(); time = fillValues(ht, node.getNodeFreeSpace(), time); /* * Add an interval that should add another sibling to the previous nodes */ ht.insert(createInterval(time, time + 1)); time += 1; assertEquals(4, ht.getNodeCount()); assertEquals(2, ht.getDepth()); /* Fill the latest leaf node (3rd and last child) */ node = ht.getLatestLeaf(); time = fillValues(ht, node.getNodeFreeSpace(), time); /* The new node created here should generate a new branch */ ht.insert(createInterval(time, time + 1)); time += 1; assertEquals(7, ht.getNodeCount()); assertEquals(3, ht.getDepth()); /* * Completely fill the second level, such that there will be a 4th level * added */ while (ht.getDepth() < 4) { time = fillNextLeafNode(ht, time); ht.insert(createInterval(time, time + 1)); } assertEquals(4, ht.getDepth()); // Assert the integrity of the tree ht.closeTree(ht.getTreeEnd()); HtTestUtils.assertTreeIntegrity(ht); } /** * Make sure the node sequence numbers and parent pointers are set correctly * when new nodes are created. * * <p> * We are building a tree whose node sequence numbers will look like this at * the end: * </p> * * <pre> * 3 * / \ * 1 4 * / \ \ * 0 2 5 * </pre> * * <p> * However while building, the parent pointers may be different. * </p> * * @throws ClosedChannelException * If the file channel is closed */ @Test public void testNodeSequenceNumbers() throws ClosedChannelException { long time = 1; AbstractHistoryTree<E, N> ht = setupSmallTree(2, time); time = fillNextLeafNode(ht, time); /* There is only one node in the tree at this point, with no parent */ List<N> branch = ht.getLatestBranch(); assertEquals(1, branch.size()); assertEquals(0, branch.get(0).getSequenceNumber()); assertEquals(-1, branch.get(0).getParentSequenceNumber()); /* Create a new branch */ ht.insert(createInterval(time, time + 1)); time = fillNextLeafNode(ht, time + 1); assertEquals(3, ht.getNodeCount()); assertEquals(2, ht.getDepth()); /* Make sure the first node's parent was updated */ N node = ht.getNode(0); assertEquals(0, node.getSequenceNumber()); assertEquals(1, node.getParentSequenceNumber()); /* Make sure the new branch is all right */ branch = ht.getLatestBranch(); assertEquals(2, branch.size()); assertEquals(1, branch.get(0).getSequenceNumber()); assertEquals(-1, branch.get(0).getParentSequenceNumber()); assertEquals(2, branch.get(1).getSequenceNumber()); assertEquals(1, branch.get(1).getParentSequenceNumber()); /* Create a third branch */ ht.insert(createInterval(time, time + 1)); time = fillNextLeafNode(ht, time + 1); assertEquals(6, ht.getNodeCount()); assertEquals(3, ht.getDepth()); /* Make sure all previous nodes are still correct */ node = ht.getNode(0); assertEquals(0, node.getSequenceNumber()); assertEquals(1, node.getParentSequenceNumber()); node = ht.getNode(1); assertEquals(1, node.getSequenceNumber()); assertEquals(3, node.getParentSequenceNumber()); node = ht.getNode(2); assertEquals(2, node.getSequenceNumber()); assertEquals(1, node.getParentSequenceNumber()); /* Verify the contents of the new latest branch */ branch = ht.getLatestBranch(); assertEquals(3, branch.size()); assertEquals(3, branch.get(0).getSequenceNumber()); assertEquals(-1, branch.get(0).getParentSequenceNumber()); assertEquals(4, branch.get(1).getSequenceNumber()); assertEquals(3, branch.get(1).getParentSequenceNumber()); assertEquals(5, branch.get(2).getSequenceNumber()); assertEquals(4, branch.get(2).getParentSequenceNumber()); // Assert the integrity of the tree ht.closeTree(ht.getTreeEnd()); HtTestUtils.assertTreeIntegrity(ht); } /** * Test reading a tree and make sure it is identical to the original one * * <p> * We are building a tree whose node sequence numbers will look like this at * the end: * </p> * * <pre> * 4 * / \ * 1 5 * / | \ \ * 0 2 3 6 * </pre> * * @throws IOException * Exceptions with the HT file * */ @Test public void testReadTree() throws IOException { long time = 1; // Build the tree for the test AbstractHistoryTree<E, N> ht = setupSmallTree(); time = fillNextLeafNode(ht, time); /* Create a new branch */ ht.insert(createInterval(time, time + 1)); time = fillNextLeafNode(ht, time + 1); /* Fill the third child */ ht.insert(createInterval(time, time + 1)); time = fillNextLeafNode(ht, time + 1); /* Make sure the tree has the expected structure at this point */ assertEquals(4, ht.getNodeCount()); assertEquals(2, ht.getDepth()); /* Add the third branch */ ht.insert(createInterval(time, time + 1)); time = fillNextLeafNode(ht, time + 1); /* Make sure the tree has the expected structure */ assertEquals(7, ht.getNodeCount()); assertEquals(3, ht.getDepth()); // Close the tree and save the nodes for later ht.closeTree(time + 1); List<N> expectedNodes = new ArrayList<>(ht.getNodeCount()); for (int i = 0; i < ht.getNodeCount(); i++) { expectedNodes.add(ht.getNode(i)); } ht.closeFile(); // Create a reader history tree ht = createHistoryTreeReader(); // Make sure the number of nodes and depth is as expected assertEquals(7, ht.getNodeCount()); assertEquals(3, ht.getDepth()); for (int i = 0; i < ht.getNodeCount(); i++) { assertEquals(expectedNodes.get(i), ht.readNode(i)); } // Assert the integrity of the read tree HtTestUtils.assertTreeIntegrity(ht); } /** * Test the tree end time */ @Test public void testTreeEnd() { long time = 1; // Check the end time at the start AbstractHistoryTree<E, N> ht = setupSmallTree(); assertEquals(time, ht.getTreeEnd()); // Fill a node and check the end time = fillNextLeafNode(ht, time); assertEquals(time, ht.getTreeEnd()); // Add an object that should not change the end time E object = createInterval(time - 10, time - 5); ht.insert(object); assertEquals(time, ht.getTreeEnd()); // Assert the tree integrity ht.closeTree(ht.getTreeEnd()); HtTestUtils.assertTreeIntegrity(ht); } }