blob: 36432db7ea7d359598233efbbb93d70c187beead [file] [log] [blame]
/*******************************************************************************
* 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 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
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.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.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
* @throws IOException failed to delete file
*/
@After
public void cleanup() throws IOException {
if (fTempFile != null) {
Files.delete(fTempFile.toPath());
}
}
/**
* 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 Objects.requireNonNull(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% */
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));
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);
}
}