diff --git a/.gitignore b/.gitignore index 5ec989b..25a8515 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +/.idea/libraries/ +/.idea/workspace.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..9d5b48d --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..bcea045 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,104 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..6dda909 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,33 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..c0bce70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/findbugs-idea.xml b/.idea/findbugs-idea.xml new file mode 100644 index 0000000..9548eb2 --- /dev/null +++ b/.idea/findbugs-idea.xml @@ -0,0 +1,213 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..31e175c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,60 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0abd9e0 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..935c7b9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index ea41f65..6b8d986 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,18 @@ CHANGELOG ========= +0.2.0 +------ + +* Demo of full-path node name +* insertInPath() uses setData() +* Dynamic node names +* New tests following mutation testing +* Tidy the Node interface +* Named nodes +* Update code style +* Drop lombok in production (still using it in test) + 0.1.0 ------ diff --git a/node.iml b/node.iml new file mode 100644 index 0000000..bb89d96 --- /dev/null +++ b/node.iml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 59ab9da..248d924 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,8 @@ 4.0.0 - net.kemitix node - 0.1.0 + 0.2.0 jar Node @@ -15,6 +14,10 @@ 0.6.0 + + 3.4.1 + + https://github.com/kemitix/node/issues GitHub Issues @@ -34,9 +37,9 @@ org.projectlombok lombok - 1.16.6 + 1.16.8 + test - junit junit @@ -49,5 +52,11 @@ 1.3 test + + org.assertj + assertj-core + ${assertj.version} + test + diff --git a/src/main/java/net/kemitix/node/Node.java b/src/main/java/net/kemitix/node/Node.java index 2dae709..9d82efd 100644 --- a/src/main/java/net/kemitix/node/Node.java +++ b/src/main/java/net/kemitix/node/Node.java @@ -7,11 +7,28 @@ import java.util.Set; /** * An interface for tree node items. * - * @author pcampbell * @param the type of data held in each node + * + * @author pcampbell */ public interface Node { + /** + * Fetch the name of the node. Where a node's name is determined via a name + * supplier, the name may be regenerated each time this method is called. + * + * @return the name of the node + */ + String getName(); + + /** + * Sets the explicit name for a node. Setting the name to null will clear + * the name and revert to the parent's name supplier. + * + * @param name the new name + */ + void setName(String name); + /** * Fetch the data held within the node. * @@ -20,8 +37,21 @@ public interface Node { T getData(); /** - * Fetch the parent node. + * Set the data held within the node. * + * @param data the node's data + */ + void setData(T data); + + /** + * Returns true if the node is empty (has no data). + * + * @return true is data is null + */ + boolean isEmpty(); + + /** + * Fetch the parent node. *

* If the node is a root node, i.e. has no parent, then this will return * null. @@ -30,6 +60,13 @@ public interface Node { */ Node getParent(); + /** + * Make the current node a direct child of the parent. + * + * @param parent the new parent node + */ + void setParent(Node parent); + /** * Fetches the child nodes. * @@ -42,16 +79,26 @@ public interface Node { * * @param child the node to add */ - void addChild(final Node child); + void addChild(Node child); /** - * Creates a new node and adds it as a child of the current node. + * Creates a new unnamed node and adds it as a child of the current node. * * @param child the child node's data * * @return the new child node */ - Node createChild(final T child); + Node createChild(T child); + + /** + * Creates a new named node and adds it as a child of the current node. + * + * @param child the child node's data + * @param name the name + * + * @return the new child node + */ + Node createChild(T child, String name); /** * Populates the tree with the path of nodes, each being a child of the @@ -59,7 +106,7 @@ public interface Node { * * @param descendants the line of descendants from the current node */ - void createDescendantLine(final List descendants); + void createDescendantLine(List descendants); /** * Looks for a child node and returns it, creating a new child node if one @@ -69,7 +116,8 @@ public interface Node { * * @return the found or created child node */ - Node findOrCreateChild(final T child); + @Deprecated + Node findOrCreateChild(T child); /** * Fetches the node for the child if present. @@ -78,7 +126,18 @@ public interface Node { * * @return an {@link Optional} containing the child node if found */ - Optional> getChild(final T child); + Optional> findChild(T child); + + /** + * Fetches the node for the child if present. + * + * @param child the child's data to search for + * + * @return the child node if found + * + * @throws NodeException if the node is not found + */ + Node getChild(T child); /** * Checks if the node is an ancestor. @@ -87,14 +146,7 @@ public interface Node { * * @return true if the node is an ancestor */ - boolean isChildOf(final Node node); - - /** - * Make the current node a direct child of the parent. - * - * @param parent the new parent node - */ - void setParent(final Node parent); + boolean isDescendantOf(Node node); /** * Walks the node tree using the path to select each child. @@ -103,6 +155,62 @@ public interface Node { * * @return the child or null */ - Optional> walkTree(final List path); + Optional> findInPath(List path); + /** + * Places the node in the tree under by the path. Intervening empty + * nodes are created as needed. + * + * @param node the node to place + * @param path the path to contain the new node + */ + void insertInPath(Node node, String... path); + + /** + * Searches for a child with the name given. + * + * @param name the name of the child + * + * @return an Optional containing the child found or empty + */ + Optional> findChildByName(String name); + + /** + * Returns the child with the given name. If one can't be found a + * NodeException is thrown. + * + * @param name the name of the child + * + * @return the node + */ + Node getChildByName(String name); + + /** + * Draw a representation of the tree. + * + * @param depth current depth for recursion + * + * @return a representation of the tree + */ + String drawTree(int depth); + + /** + * Returns true if the Node has a name. Where a name supplier is used, the + * generated name is used. + * + * @return true if the node has a name + */ + boolean isNamed(); + + /** + * Remove the node from the children. + * + * @param node the node to be removed + */ + void removeChild(Node node); + + /** + * Removes the parent from the node. Makes the node into a new root node. + */ + void removeParent(); } diff --git a/src/main/java/net/kemitix/node/NodeItem.java b/src/main/java/net/kemitix/node/NodeItem.java index d35366a..b724048 100644 --- a/src/main/java/net/kemitix/node/NodeItem.java +++ b/src/main/java/net/kemitix/node/NodeItem.java @@ -1,12 +1,11 @@ package net.kemitix.node; -import lombok.Getter; -import lombok.NonNull; - +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; /** * Represents a tree of nodes. @@ -17,53 +16,136 @@ import java.util.Set; */ public class NodeItem implements Node { - @Getter - private final T data; + private T data; + + private final Set> children = new HashSet<>(); + + private Function, String> nameSupplier; - @Getter private Node parent; - @Getter - private Set> children; + private String name; /** - * Creates a root node. + * Create named root node. * - * @param data the value of the node + * @param data the data or null + * @param name the name */ - public NodeItem(@NonNull final T data) { - this(data, null); + public NodeItem(final T data, final String name) { + this(data); + this.name = name; + } + + /** + * Create unnamed root node. + * + * @param data the data or null + */ + public NodeItem(final T data) { + this.data = data; + this.nameSupplier = (n) -> null; + } + + /** + * Creates root node with a name supplier. + * + * @param data the data or null + * @param nameSupplier the name supplier function + */ + public NodeItem( + final T data, final Function, String> nameSupplier) { + this(data); + this.nameSupplier = nameSupplier; } /** * Creates a node with a parent. * - * @param data the value of the node + * @param data the data or null * @param parent the parent node */ public NodeItem(final T data, final Node parent) { this.data = data; - if (parent != null) { - setParent(parent); - } - this.children = new HashSet<>(); + setParent(parent); } /** - * Make the current node a direct child of the parent. + * Creates a named node with a parent. * - * @param parent the new parent node + * @param data the data or null + * @param name the name + * @param parent the parent node */ + public NodeItem(final T data, final String name, final Node parent) { + this.data = data; + this.name = name; + setParent(parent); + } + + /** + * Creates a node with a name supplier and a parent. + * + * @param data the data or null + * @param nameSupplier the name supplier function + * @param parent the parent node + */ + public NodeItem( + final T data, final Function, String> nameSupplier, + final Node parent) { + this(data, nameSupplier); + setParent(parent); + } + + private String generateName() { + return getNameSupplier().apply(this); + } + + private Function, String> getNameSupplier() { + if (nameSupplier != null) { + return nameSupplier; + } + // no test for parent as root nodes will always have a default name + // supplier + return ((NodeItem) parent).getNameSupplier(); + } + @Override - public final void setParent(@NonNull final Node parent) { - if (this.equals(parent) || parent.isChildOf(this)) { - throw new NodeException("Parent is a descendant"); + public String getName() { + if (name == null) { + return generateName(); } - if (this.parent != null) { - this.parent.getChildren().remove(this); - } - this.parent = parent; - parent.addChild(this); + return name; + } + + @Override + public void setName(final String name) { + this.name = name; + } + + @Override + public T getData() { + return data; + } + + @Override + public void setData(final T data) { + this.data = data; + } + + @Override + public boolean isEmpty() { + return data == null; + } + + @Override + public Node getParent() { + return parent; + } + + @Override + public Set> getChildren() { + return children; } /** @@ -72,10 +154,21 @@ public class NodeItem implements Node { * @param child the node to add */ @Override - public void addChild(@NonNull final Node child) { - if (this.equals(child) || isChildOf(child)) { + public void addChild(final Node child) { + if (child == null) { + throw new NullPointerException("child"); + } + if (this.equals(child) || isDescendantOf(child)) { throw new NodeException("Child is an ancestor"); } + if (child.isNamed()) { + final Optional> existingChild = findChildByName( + child.getName()); + if (existingChild.isPresent() && existingChild.get() != child) { + throw new NodeException( + "Node with that name already exists here"); + } + } children.add(child); if (child.getParent() == null || !child.getParent().equals(this)) { child.setParent(this); @@ -83,45 +176,26 @@ public class NodeItem implements Node { } /** - * Checks if the node is an ancestor. + * Creates a new node and adds it as a child of the current node. * - * @param node the potential ancestor + * @param child the child node's data * - * @return true if the node is an ancestor + * @return the new child node */ @Override - public boolean isChildOf(final Node node) { - if (node.equals(parent)) { - return true; + public Node createChild(final T child) { + if (child == null) { + throw new NullPointerException("child"); } - if (parent != null) { - return parent.isChildOf(node); - } - return false; + return new NodeItem<>(child, this); } - /** - * Walks the node tree using the path to select each child. - * - * @param path the path to the desired child - * - * @return the child or null - */ @Override - public Optional> walkTree(@NonNull final List path) { - if (path.size() > 0) { - Optional> found = children.stream() - .filter((Node child) -> path.get(0) - .equals(child.getData())) - .findFirst(); - if (found.isPresent()) { - if (path.size() > 1) { - return found.get().walkTree(path.subList(1, path.size())); - } - return found; - } - } - return Optional.empty(); + @SuppressWarnings("hiddenfield") + public Node createChild(final T child, final String name) { + Node node = createChild(child); + node.setName(name); + return node; } /** @@ -131,11 +205,13 @@ public class NodeItem implements Node { * @param descendants the line of descendants from the current node */ @Override - public void createDescendantLine(@NonNull final List descendants) { + public void createDescendantLine(final List descendants) { + if (descendants == null) { + throw new NullPointerException("descendants"); + } if (!descendants.isEmpty()) { - findOrCreateChild(descendants.get(0)) - .createDescendantLine( - descendants.subList(1, descendants.size())); + findOrCreateChild(descendants.get(0)).createDescendantLine( + descendants.subList(1, descendants.size())); } } @@ -148,13 +224,11 @@ public class NodeItem implements Node { * @return the found or created child node */ @Override - public Node findOrCreateChild(@NonNull final T child) { - Optional> found = getChild(child); - if (found.isPresent()) { - return found.get(); - } else { - return createChild(child); + public Node findOrCreateChild(final T child) { + if (child == null) { + throw new NullPointerException("child"); } + return findChild(child).orElseGet(() -> createChild(child)); } /** @@ -165,22 +239,176 @@ public class NodeItem implements Node { * @return an {@link Optional} containing the child node if found */ @Override - public Optional> getChild(@NonNull final T child) { + public Optional> findChild(final T child) { + if (child == null) { + throw new NullPointerException("child"); + } return children.stream() - .filter((Node t) -> t.getData().equals(child)) - .findAny(); + .filter((Node t) -> t.getData().equals(child)) + .findAny(); + } + + @Override + public Node getChild(final T child) { + Optional> optional = findChild(child); + if (optional.isPresent()) { + return optional.get(); + } + throw new NodeException("Child not found"); } /** - * Creates a new node and adds it as a child of the current node. + * Checks if the node is an ancestor. * - * @param child the child node's data + * @param node the potential ancestor * - * @return the new child node + * @return true if the node is an ancestor */ @Override - public Node createChild(@NonNull final T child) { - return new NodeItem<>(child, this); + public boolean isDescendantOf(final Node node) { + return parent != null && (node.equals(parent) || parent.isDescendantOf( + node)); + } + + /** + * Make the current node a direct child of the parent. + * + * @param parent the new parent node + */ + @Override + public final void setParent(final Node parent) { + if (parent == null) { + throw new NullPointerException("parent"); + } + if (this.equals(parent) || parent.isDescendantOf(this)) { + throw new NodeException("Parent is a descendant"); + } + if (this.parent != null) { + this.parent.getChildren().remove(this); + } + this.parent = parent; + parent.addChild(this); + } + + /** + * Walks the node tree using the path to select each child. + * + * @param path the path to the desired child + * + * @return the child or null + */ + @Override + public Optional> findInPath(final List path) { + if (path == null) { + throw new NullPointerException("path"); + } + if (path.size() > 0) { + Optional> found = findChild(path.get(0)); + if (found.isPresent()) { + if (path.size() > 1) { + return found.get().findInPath(path.subList(1, path.size())); + } + return found; + } + } + return Optional.empty(); + } + + @Override + public void insertInPath(final Node nodeItem, final String... path) { + if (path.length == 0) { + if (!nodeItem.isNamed()) { // nothing to conflict with + addChild(nodeItem); + return; + } + final Optional> childNamed = findChildByName( + nodeItem.getName()); + if (!childNamed.isPresent()) { // nothing with the same name exists + addChild(nodeItem); + return; + } + // we have an existing node with the same name + final Node existing = childNamed.get(); + if (!existing.isEmpty()) { + throw new NodeException( + "A non-empty node with that name already exists here"); + } else { + existing.setData(nodeItem.getData()); + } + return; + } + String item = path[0]; + final Optional> childNamed = findChildByName(item); + Node child; + if (!childNamed.isPresent()) { + child = new NodeItem<>(null, item, this); + } else { + child = childNamed.get(); + } + child.insertInPath(nodeItem, Arrays.copyOfRange(path, 1, path.length)); + } + + @Override + public Optional> findChildByName(final String named) { + if (named == null) { + throw new NullPointerException("name"); + } + return children.stream() + .filter((Node t) -> t.getName().equals(named)) + .findAny(); + } + + @Override + public Node getChildByName(final String named) { + final Optional> optional = findChildByName(named); + if (optional.isPresent()) { + return optional.get(); + } + throw new NodeException("Named child not found"); + } + + @Override + public String drawTree(final int depth) { + final StringBuilder sb = new StringBuilder(); + final String unnamed = "(unnamed)"; + if (isNamed()) { + sb.append(String.format("[%1$" + (depth + name.length()) + "s]\n", + name)); + } else if (!children.isEmpty()) { + sb.append( + String.format("[%1$" + (depth + unnamed.length()) + "s]\n", + unnamed)); + } + getChildren().stream().forEach(c -> sb.append(c.drawTree(depth + 1))); + return sb.toString(); + } + + @Override + public boolean isNamed() { + String currentName = getName(); + return currentName != null && currentName.length() > 0; + } + + @Override + public void removeChild(final Node node) { + if (children.remove(node)) { + node.removeParent(); + } + } + + @Override + public void removeParent() { + if (parent != null) { + Node oldParent = parent; + Function, String> supplier = getNameSupplier(); + parent = null; + oldParent.removeChild(this); + if (this.nameSupplier == null) { + // this is now a root node, so must provide a default name + // supplier + this.nameSupplier = supplier; + } + } } } diff --git a/src/test/java/net/kemitix/node/NodeExceptionTest.java b/src/test/java/net/kemitix/node/NodeExceptionTest.java index 5e9f7ac..2d1422b 100644 --- a/src/test/java/net/kemitix/node/NodeExceptionTest.java +++ b/src/test/java/net/kemitix/node/NodeExceptionTest.java @@ -1,7 +1,6 @@ package net.kemitix.node; -import net.kemitix.node.NodeException; - +import lombok.val; import org.junit.Test; import static org.hamcrest.CoreMatchers.is; @@ -14,20 +13,15 @@ import static org.junit.Assert.assertThat; */ public class NodeExceptionTest { - /** - * Class under test. - */ - private NodeException nodeException; - /** * Test that message provided to constructor is returned. */ @Test public void shouldReturnConstructorMessage() { //given - final String message = "this is the message"; + val message = "this is the message"; //when - nodeException = new NodeException(message); + val nodeException = new NodeException(message); //then assertThat(nodeException.getMessage(), is(message)); } diff --git a/src/test/java/net/kemitix/node/NodeItemTest.java b/src/test/java/net/kemitix/node/NodeItemTest.java index c271ac3..b292c9d 100644 --- a/src/test/java/net/kemitix/node/NodeItemTest.java +++ b/src/test/java/net/kemitix/node/NodeItemTest.java @@ -1,17 +1,20 @@ package net.kemitix.node; +import lombok.val; +import org.assertj.core.api.SoftAssertions; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collections; import java.util.Optional; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; /** * Test for {@link NodeItem}. @@ -20,477 +23,1012 @@ import static org.junit.Assert.assertTrue; */ public class NodeItemTest { - /** - * Class under test. - */ + @Rule + public ExpectedException exception = ExpectedException.none(); + private Node node; - /** - * Test {@link NodeItem#Node(java.lang.Object) } that node data is - * recoverable. - */ @Test - public void shouldReturnNodeData() { + public void getDataReturnsData() { //given - final String data = "this node data"; + val data = "this node data"; //when node = new NodeItem<>(data); //then - assertThat(node.getData(), is(data)); + assertThat(node.getData()).as("can get the data from a node"). + isSameAs(data); } - /** - * Test {@link NodeItem#Node(java.lang.Object) } that passing null as node - * data throws exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenDataIsNull() { + @Test + public void canCreateAnEmptyAndUnnamedNode() { //when node = new NodeItem<>(null); + //then + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(node.isEmpty()).as("node is empty").isTrue(); + softly.assertThat(node.isNamed()).as("node is unnamed").isFalse(); + softly.assertAll(); + } + + @Test + public void canCreateNodeWithParentAndCustomNameSupplier() { + //given + node = new NodeItem<>(null, n -> "root name supplier"); + //when + val child = new NodeItem<>(null, n -> "overridden", node); + //then + assertThat(child.getName()).isEqualTo("overridden"); + } + + @Test + public void canSetName() { + //given + node = new NodeItem<>(null); + //when + node.setName("named"); + //then + assertThat(node.getName()).isEqualTo("named"); } /** - * Test {@link NodeItem#Node(java.lang.Object) } that default node parent is - * null. + * Test that default node parent is null. */ @Test - public void shouldHaveNullForDefaulParent() { + public void shouldHaveNullForDefaultParent() { //given - node = new NodeItem<>("data"); + node = new NodeItem<>("data", Node::getData); //then - assertNull(node.getParent()); + assertThat(node.getParent()).as( + "node created without a parent has null as parent").isNull(); } /** - * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } that - * provided node parent is returned. + * Test that provided node parent is returned. */ @Test public void shouldReturnNodeParent() { //given - Node parent = new NodeItem<>("parent"); + val parent = new NodeItem("parent", Node::getData); //when node = new NodeItem<>("subject", parent); //then - assertThat(node.getParent(), is(parent)); + assertThat(node.getParent()).as( + "node created with a parent can return the parent") + .isSameAs(parent); } /** - * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } that - * setting the parent on a node where the proposed parent is a child of the - * node throws an exception. + * Test that setting the parent on a node where the proposed parent is a + * child of the node throws an exception. */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenSettingParentToAChild() { + @Test + public void setParentShouldThrowNodeExceptionWhenParentIsAChild() { //given - node = new NodeItem<>("subject"); - Node child = new NodeItem<>("child", node); + node = new NodeItem<>("subject", Node::getData); + val child = new NodeItem("child", node); + exception.expect(NodeException.class); + exception.expectMessage("Parent is a descendant"); //when node.setParent(child); } /** - * Test {@link NodeItem#Node(java.lang.Object, net.kemitix.node.Node) } that - * when parent is added to created node, the created node is now a child of - * the parent. + * Test that when parent is added to created node, the created node is now a + * child of the parent. */ @Test + @SuppressWarnings("unchecked") public void shouldAddNewNodeAsChildToParent() { //given - Node parent = new NodeItem<>("parent"); + val parent = new NodeItem("parent", Node::getData); //when node = new NodeItem<>("subject", parent); //then - assertThat(parent.getChildren(), hasItem(node)); + assertThat(parent.getChildren()).as( + "when a node is created with a parent, the parent has the new" + + " node among it's children").contains(node); } /** - * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we return - * the same parent when set. + * Test that we return the same parent when set. */ @Test public void shouldReturnSetParent() { //given - node = new NodeItem<>("subject"); - Node parent = new NodeItem<>("parent"); + node = new NodeItem<>("subject", Node::getData); + val parent = new NodeItem("parent", Node::getData); //when node.setParent(parent); //then - assertThat(node.getParent(), is(parent)); - } - - /** - * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we throw an - * exception when passed null. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenSetParentNull() { - //given - node = new NodeItem<>("subject"); - //when - node.setParent(null); - } - - /** - * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that we throw an - * exceptions when attempting to node as its own parent. - */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenSetParentSelf() { - //given - node = new NodeItem<>("subject"); - //when - node.setParent(node); - } - - /** - * Test {@link NodeItem#setParent(net.kemitix.node.Node) } that when a node - * with an existing parent is assigned a new parent, that the old parent no - * longer sees it as one of its children. - */ - @Test - public void shouldUpdateOldParentWhenNodeSetToNewParent() { - //given - node = new NodeItem<>("subject"); - Node child = node.createChild("child"); - Node newParent = new NodeItem<>("newParent"); - //when - child.setParent(newParent); - //then - assertThat(child.getParent(), is(newParent)); - assertFalse(node.getChild("child").isPresent()); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that when a node - * is added as a child to another node, that it's previous parent no longer - * has it as a child. - */ - @Test - public void shouldRemoveNodeFromOldParentWhenAddedAsChildToNewParent() { - //given - node = new NodeItem<>("subject"); - Node child = node.createChild("child"); - Node newParent = new NodeItem<>("newParent"); - //when - newParent.addChild(child); - //then - assertThat(child.getParent(), is(newParent)); - assertFalse(node.getChild("child").isPresent()); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding null - * as a child throws an exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenAddingNullAsChild() { - //given - node = new NodeItem<>("subject"); - //when - node.addChild(null); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a - * child is returned. - */ - @Test - public void shouldReturnAddedChild() { - //given - Node child = new NodeItem<>("child"); - node = new NodeItem<>("subject"); - //when - node.addChild(child); - //then - assertThat(node.getChildren(), hasItem(child)); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a node - * as it's own child throws an exception. - */ - @Test(expected = NodeException.class) - public void shouldThrowNEWhenAddingANodeAsOwnChild() { - //given - node = new NodeItem<>("subject"); - //then - node.addChild(node); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a node - * to itself as a child causes an exception. - */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingSelfAsChild() { - //given - node = new NodeItem<>("subject"); - //when - node.addChild(node); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding the - * parent to node causes an exception. - */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingParentAsChild() { - //given - Node parent = new NodeItem<>("parent"); - node = new NodeItem<>("subject", parent); - //when - node.addChild(parent); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding the - * grandparent to node causes an exception. - */ - @Test(expected = NodeException.class) - public void shouldThrowWhenAddingGrandParentAsChild() { - //given - Node grandParent = new NodeItem<>("grandparent"); - Node parent = new NodeItem<>("parent", grandParent); - node = new NodeItem<>("subject", parent); - //when - node.addChild(grandParent); - } - - /** - * Test {@link NodeItem#addChild(net.kemitix.node.Node) } that adding a - * child to a node, sets the child's parent node. - */ - @Test - public void shouldSetParentOnChildWhenAddedAsChild() { - //given - Node child = new NodeItem<>("child"); - node = new NodeItem<>("subject"); - //when - node.addChild(child); - //then - assertThat(child.getParent(), is(node)); - } - - /** - * Test {@link NodeItem#walkTree(java.util.List) } that we can walk a tree - * to the target node. - */ - @Test - public void shouldWalkTreeToNode() { - //given - final String grandparent = "grandparent"; - Node grandParentNode = new NodeItem<>(grandparent); - final String parent = "parent"; - Node parentNode = new NodeItem<>(parent, grandParentNode); - final String subject = "subject"; - node = new NodeItem<>(subject, parentNode); - //when - Optional> result = grandParentNode.walkTree(Arrays.asList( - parent, subject)); - //then - assertTrue(result.isPresent()); - assertThat(result.get(), is(node)); - } - - /** - * Test {@link NodeItem#walkTree(java.util.List) } that we get an empty - * {@link Optional} when walking a path that doesn't exist. - */ - @Test - public void shouldNotFindNonExistantChildNode() { - //given - final String parent = "parent"; - Node parentNode = new NodeItem<>(parent); - final String subject = "subject"; - node = new NodeItem<>(subject, parentNode); - //when - Optional> result = parentNode.walkTree(Arrays.asList( - subject, "no child")); - //then - assertFalse(result.isPresent()); - } - - /** - * Test {@link NodeItem#walkTree(java.util.List) } that when we pass null we - * get an exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNEWhenWalkTreeNull() { - //given - node = new NodeItem<>("subject"); - //when - node.walkTree(null); - } - - /** - * Test {@link NodeItem#walkTree(java.util.List) } that when we pass an - * empty path we get and empty {@link Optional} as a result. - */ - @Test - public void shouldReturnEmptyForEmptyWalkTreePath() { - //given - node = new NodeItem<>("subject"); - //when - node.walkTree(Collections.emptyList()); - } - - /** - * Test {@link NodeItem#createDescendantLine(java.util.List) } that we can - * create a chain of descendant nodes. - */ - @Test - public void shouldCreateDescendantNodes() { - //given - node = new NodeItem<>("subject"); - final String alphaData = "alpha"; - final String betaData = "beta"; - final String gammaData = "gamma"; - //when - node.createDescendantLine( - Arrays.asList(alphaData, betaData, gammaData)); - //then - final Optional> alphaOptional = node.getChild(alphaData); - assertTrue(alphaOptional.isPresent()); - Node alpha = alphaOptional.get(); - assertThat(alpha.getParent(), is(node)); - final Optional> betaOptional = alpha.getChild(betaData); - assertTrue(betaOptional.isPresent()); - Node beta = betaOptional.get(); - assertThat(beta.getParent(), is(alpha)); - final Optional> gammaOptional = beta.getChild(gammaData); - assertTrue(gammaOptional.isPresent()); - Node gamma = gammaOptional.get(); - assertThat(gamma.getParent(), is(beta)); - } - - /** - * Test {@link NodeItem#createDescendantLine(java.util.List) } that if we - * pass null to create a chain of descendant nodes we get an exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenCreateDescendantNull() { - //given - node = new NodeItem<>("subject"); - //when - node.createDescendantLine(null); - } - - /** - * Test {@link NodeItem#createDescendantLine(java.util.List) } that if we - * pass an empty list nothing is changed. - */ - @Test - public void shouldChangeNothingWhenCreateDescendantEmpty() { - //given - node = new NodeItem<>("subject"); - //when - node.createDescendantLine(Collections.emptyList()); - //then - assertThat(node.getChildren().size(), is(0)); - } - - /** - * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that we can - * find a child of a node. - */ - @Test - public void shouldFindExistingChildNode() { - //given - node = new NodeItem<>("subject"); - final String childData = "child"; - Node child = new NodeItem<>(childData, node); - //when - Node found = node.findOrCreateChild(childData); - //then - assertThat(found, is(child)); - } - - /** - * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that we create - * a missing child of a node. - */ - @Test - public void shouldFindCreateNewChildNode() { - //given - node = new NodeItem<>("subject"); - final String childData = "child"; - //when - Node found = node.findOrCreateChild(childData); - //then - assertThat(found.getData(), is(childData)); - } - - /** - * Test {@link NodeItem#findOrCreateChild(java.lang.Object) } that if we - * pass null we get an exception. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEFWhenFindOrCreateChildNull() { - //given - node = new NodeItem<>("subject"); - //when - node.findOrCreateChild(null); - } - - /** - * Test {@link NodeItem#getChild(java.lang.Object) } that we can get the - * node for a child. - */ - @Test - public void shouldGetChild() { - //given - node = new NodeItem<>("subject"); - final String childData = "child"; - Node child = new NodeItem<>(childData); - node.addChild(child); - //when - Optional> found = node.getChild(childData); - //then - assertTrue(found.isPresent()); - assertThat(found.get(), is(child)); - } - - /** - * Test {@link NodeItem#getChild(java.lang.Object) } that we throw an - * exception when passed null. - */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenGetChildNull() { - //given - node = new NodeItem<>("subject"); - //when - node.getChild(null); - } - - /** - * Test {@link NodeItem#createChild(java.lang.Object) } that we create a - * child as a child of the current node and with the current node as its - * parent. - */ - @Test - public void shoudCreateChild() { - //given - node = new NodeItem<>("subject"); - final String childData = "child"; - //when - Node child = node.createChild(childData); - //then - assertThat(child.getParent(), is(node)); - final Optional> foundChild = node.getChild(childData); - assertTrue(foundChild.isPresent()); - assertThat(foundChild.get(), is(child)); + assertThat(node.getParent()).as( + "when a node is assigned a new parent that parent can be " + + "returned").isSameAs(parent); } /** * Test that we throw an exception when passed null. */ - @Test(expected = NullPointerException.class) - public void shouldThrowNPEWhenCreateChildNull() { + @Test + public void shouldThrowNPEWhenSetParentNull() { //given - node = new NodeItem<>("subject"); + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("parent"); + //when + node.setParent(null); + } + + /** + * Test that we throw an exceptions when attempting to node as its own + * parent. + */ + @Test + public void setParentShouldThrowNodeExceptionWhenParentIsSelf() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Parent is a descendant"); + //when + node.setParent(node); + } + + /** + * Test that when a node with an existing parent is assigned a new parent, + * that the old parent no longer sees it as one of its children. + */ + @Test + public void shouldUpdateOldParentWhenNodeSetToNewParent() { + //given + node = new NodeItem<>("subject", Node::getData); + val child = node.createChild("child"); + val newParent = new NodeItem("newParent", Node::getData); + //when + child.setParent(newParent); + //then + assertThat(child.getParent()).as( + "when a node is assigned a new parent, the old parent is " + + "replaced").isSameAs(newParent); + assertThat(node.findChild("child").isPresent()).as( + "when a node is assigned a new parent, the old parent no " + + "longer has the node among it's children").isFalse(); + } + + /** + * Test that when a node is added as a child to another node, that it's + * previous parent no longer has it as a child. + */ + @Test + public void shouldRemoveNodeFromOldParentWhenAddedAsChildToNewParent() { + //given + node = new NodeItem<>("subject", Node::getData); + val child = node.createChild("child"); + val newParent = new NodeItem("newParent", Node::getData); + //when + newParent.addChild(child); + //then + assertThat(child.getParent()).as( + "when a node with an existing parent is added as a child " + + "to another node, then the old parent is replaced") + .isSameAs(newParent); + assertThat(node.findChild("child").isPresent()).as( + "when a node with an existing parent is added as a child to " + + "another node, then the old parent no longer has " + + "the node among it's children").isFalse(); + } + + /** + * Test that adding null as a child throws an exception. + */ + @Test + public void shouldThrowNPEWhenAddingNullAsChild() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); + //when + node.addChild(null); + } + + /** + * Test that adding a child is returned. + */ + @Test + @SuppressWarnings("unchecked") + public void shouldReturnAddedChild() { + //given + node = new NodeItem<>("subject", Node::getData); + val child = new NodeItem("child", Node::getData); + //when + node.addChild(child); + //then + assertThat(node.getChildren()).as( + "when a node is added as a child, the node is among the " + + "children").contains(child); + } + + /** + * Test that adding a node as it's own child throws an exception. + */ + @Test + public void addChildShouldThrowNodeExceptionWhenAddingANodeAsOwnChild() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); + //then + node.addChild(node); + } + + /** + * Test that adding a node to itself as a child causes an exception. + */ + @Test + public void addChildShouldThrowNodeExceptionWhenAddingSelfAsChild() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); + //when + node.addChild(node); + } + + /** + * Test that adding the parent of a node to the node as a child causes an + * exception. + */ + @Test + public void addChildShouldThrowNodeExceptionWhenChildIsParent() { + //given + val parent = new NodeItem("parent", Node::getData); + node = new NodeItem<>("subject", parent); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); + //when + node.addChild(parent); + } + + /** + * Test that adding the grandparent to a node as a child causes an + * exception. + */ + @Test + public void addChildShouldThrowNodeExceptionWhenAddingGrandParentAsChild() { + //given + val grandParent = new NodeItem("grandparent", Node::getData); + val parent = new NodeItem("parent", grandParent); + node = new NodeItem<>("subject", parent); + exception.expect(NodeException.class); + exception.expectMessage("Child is an ancestor"); + //when + node.addChild(grandParent); + } + + /** + * Test that adding a child to a node, sets the child's parent node. + */ + @Test + public void shouldSetParentOnChildWhenAddedAsChild() { + //given + val child = new NodeItem("child", Node::getData); + node = new NodeItem<>("subject", Node::getData); + //when + node.addChild(child); + //then + assertThat(child.getParent()).as( + "when a node is added as a child, the child has the node as " + + "its parent").isSameAs(node); + } + + /** + * Test that we can walk a tree to the target node. + */ + @Test + public void shouldWalkTreeToNode() { + //given + val grandparent = "grandparent"; + val grandParentNode = new NodeItem(grandparent, Node::getData); + val parent = "parent"; + val parentNode = new NodeItem(parent, grandParentNode); + val subject = "subject"; + node = new NodeItem<>(subject, parentNode); + //when + val result = grandParentNode.findInPath(Arrays.asList(parent, subject)); + //then + assertThat(result.isPresent()).as( + "when we walk the tree to a node it is found").isTrue(); + if (result.isPresent()) { + assertThat(result.get()).as( + "when we walk the tree to a node the correct node is found") + .isSameAs(node); + } + } + + /** + * Test that we get an empty {@link Optional} when walking a path that + * doesn't exist. + */ + @Test + public void shouldNotFindNonExistentChildNode() { + //given + val parent = "parent"; + val parentNode = new NodeItem(parent, Node::getData); + val subject = "subject"; + node = new NodeItem<>(subject, parentNode); + //when + val result = parentNode.findInPath(Arrays.asList(subject, "no child")); + //then + assertThat(result.isPresent()).as( + "when we walk the tree to a node that doesn't exists, nothing" + + " is found").isFalse(); + } + + /** + * Test that when we pass null we get an exception. + */ + @Test + public void shouldThrowNEWhenWalkTreeNull() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("path"); + //when + node.findInPath(null); + } + + /** + * Test that when we pass an empty path we get and empty {@link Optional} as + * a result. + */ + @Test + public void shouldReturnEmptyForEmptyWalkTreePath() { + //given + node = new NodeItem<>("subject", Node::getData); + //when + val result = node.findInPath(Collections.emptyList()); + //then + assertThat(result).isEmpty(); + } + + /** + * Test that we can create a chain of descendant nodes. + */ + @Test + public void shouldCreateDescendantNodes() { + //given + node = new NodeItem<>("subject", Node::getData); + val alphaData = "alpha"; + val betaData = "beta"; + val gammaData = "gamma"; + //when + node.createDescendantLine( + Arrays.asList(alphaData, betaData, gammaData)); + //then + val alphaOptional = node.findChild(alphaData); + assertThat(alphaOptional.isPresent()).as( + "when creating a descendant line, the first element is found") + .isTrue(); + if (alphaOptional.isPresent()) { + val alpha = alphaOptional.get(); + assertThat(alpha.getParent()).as( + "when creating a descendant line, the first element has " + + "the current node as its parent").isSameAs(node); + val betaOptional = alpha.findChild(betaData); + assertThat(betaOptional.isPresent()).as( + "when creating a descendant line, the second element is " + + "found").isTrue(); + if (betaOptional.isPresent()) { + val beta = betaOptional.get(); + assertThat(beta.getParent()).as( + "when creating a descendant line, the second element " + + "has the first as its parent") + .isSameAs(alpha); + val gammaOptional = beta.findChild(gammaData); + assertThat(gammaOptional.isPresent()).as( + "when creating a descendant line, the third element " + + "is found").isTrue(); + if (gammaOptional.isPresent()) { + val gamma = gammaOptional.get(); + assertThat(gamma.getParent()).as( + "when creating a descendant line, the third " + + "element has the second as its parent") + .isSameAs(beta); + } + } + } + } + + /** + * Test that if we pass null to create a chain of descendant nodes we get an + * exception. + */ + @Test + public void createDescendantLineShouldThrowNPEWhenDescendantsAreNull() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("descendants"); + //when + node.createDescendantLine(null); + } + + /** + * Test that if we pass an empty list nothing is changed. + */ + @Test + public void shouldChangeNothingWhenCreateDescendantEmpty() { + //given + node = new NodeItem<>("subject", Node::getData); + //when + node.createDescendantLine(Collections.emptyList()); + //then + assertThat(node.getChildren()).as( + "when creating a descendant line from an empty list, nothing " + + "is created").isEmpty(); + } + + /** + * Test that we can find a child of a node. + */ + @Test + public void shouldFindExistingChildNode() { + //given + node = new NodeItem<>("subject", Node::getData); + val childData = "child"; + val child = new NodeItem(childData, node); + //when + val found = node.findOrCreateChild(childData); + //then + assertThat(found).as( + "when searching for a child by data, the matching child is " + + "found").isSameAs(child); + } + + /** + * Test that we create a missing child of a node. + */ + @Test + public void shouldFindCreateNewChildNode() { + //given + node = new NodeItem<>("subject", Node::getData); + val childData = "child"; + //when + val found = node.findOrCreateChild(childData); + //then + assertThat(found.getData()).as( + "when searching for a child by data, a new node is created") + .isSameAs(childData); + } + + /** + * Test that if we pass null we get an exception. + */ + @Test + public void findOrCreateChildShouldThrowNPEFWhenChildIsNull() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); + //when + node.findOrCreateChild(null); + } + + /** + * Test that we can get the node for a child. + */ + @Test + public void shouldGetChild() { + //given + node = new NodeItem<>("subject", Node::getData); + val childData = "child"; + val child = new NodeItem(childData, Node::getData); + node.addChild(child); + //when + val found = node.findChild(childData); + //then + assertThat(found.isPresent()).as( + "when retrieving a child by its data, it is found").isTrue(); + if (found.isPresent()) { + assertThat(found.get()).as( + "when retrieving a child by its data, it is the expected " + + "node").isSameAs(child); + } + } + + /** + * Test that we throw an exception when passed null. + */ + @Test + public void getChildShouldThrowNPEWhenThereIsNoChild() { + //given + node = new NodeItem<>("data", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); + //when + node.findChild(null); + } + + /** + * Test that we create a child as a child of the current node and with the + * current node as its parent. + */ + @Test + public void shouldCreateChild() { + //given + node = new NodeItem<>("subject", Node::getData); + val childData = "child"; + //when + val child = node.createChild(childData); + //then + assertThat(child.getParent()).as( + "when creating a child node, the child has the current node " + + "as its parent").isSameAs(node); + val foundChild = node.findChild(childData); + assertThat(foundChild.isPresent()).as( + "when creating a child node, the child can be found by its " + + "data").isTrue(); + if (foundChild.isPresent()) { + assertThat(foundChild.get()).as( + "when creating a child node, the correct child can be " + + "found by its data").isSameAs(child); + } + } + + /** + * Test that we throw an exception when passed null. + */ + @Test + public void createChildShouldThrowNPEWhenChildIsNull() { + //given + node = new NodeItem<>("subject", Node::getData); + exception.expect(NullPointerException.class); + exception.expectMessage("child"); //when node.createChild(null); } + @Test + public void getNameShouldBeCorrect() { + //given + node = new NodeItem<>("subject", Node::getData); + //then + assertThat(node.getName()).isEqualTo("subject"); + } + + @Test + public void getNameShouldUseParentNameSupplier() { + //given + val root = new NodeItem("root", Node::getData); + node = new NodeItem<>("child", root); + //then + assertThat(node.getName()).isEqualTo("child"); + } + + @Test + public void getNameShouldReturnNameForNonStringData() { + val root = new NodeItem(LocalDate.parse("2016-05-23"), + n -> n.getData().format(DateTimeFormatter.BASIC_ISO_DATE)); + //then + assertThat(root.getName()).isEqualTo("20160523"); + } + + @Test + public void getNameShouldUseClosestNameSupplier() { + node = new NodeItem<>("root", Node::getData); + val child = new NodeItem("child", Object::toString); + node.addChild(child); + val grandChild = new NodeItem<>("grandchild", child); + //then + assertThat(node.getName()).isEqualTo("root"); + assertThat(child.getName()).isNotEqualTo("child"); + assertThat(grandChild.getName()).isNotEqualTo("grandchild"); + } + + @Test + public void getNameShouldWorkWithoutNameSupplier() { + node = new NodeItem<>(null, "root"); + val namedchild = new NodeItem<>("named", "Alice", node); + //then + assertThat(node.getName()).isEqualTo("root"); + assertThat(namedchild.getName()).isEqualTo("Alice"); + } + + @Test + public void canCreateRootNodeWithoutData() { + node = new NodeItem<>(null, "empty"); + assertThat(node.getData()).isNull(); + } + + @Test + public void canCreateRootNodeWithoutDataButWithNameSupplier() { + node = new NodeItem<>(null, Node::getData); + assertThat(node.getData()).isNull(); + } + + @Test + public void getChildNamedFindsChild() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + val beta = new NodeItem(null, "beta"); + node.addChild(alpha); + node.addChild(beta); + //when + val result = node.getChildByName("alpha"); + //then + assertThat(result).isSameAs(alpha); + } + + @Test + public void getChildNamedFindsNothing() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + val beta = new NodeItem(null, "beta"); + node.addChild(alpha); + node.addChild(beta); + exception.expect(NodeException.class); + exception.expectMessage("Named child not found"); + //when + node.getChildByName("gamma"); + } + + @Test + public void nodeNamesAreUniqueWithinAParent() { + //given + node = new NodeItem<>(null, "root"); + val alpha = new NodeItem(null, "alpha"); + node.addChild(alpha); + val beta = new NodeItem(null, "alpha"); + exception.expect(NodeException.class); + exception.expectMessage("Node with that name already exists here"); + //when + node.addChild(beta); + } + + @Test + public void canPlaceNodeInTreeByPathNames() { + //given + node = new NodeItem<>(null, "root"); // create a root + val four = new NodeItem("data", "four"); + //when + node.insertInPath(four, "one", "two", "three"); + //then + val three = four.getParent(); + assertThat(four.getParent()).as("add node to a tree").isNotNull(); + assertThat(three.getName()).isEqualTo("three"); + val two = three.getParent(); + assertThat(two.getName()).isEqualTo("two"); + val one = two.getParent(); + assertThat(one.getName()).isEqualTo("one"); + assertThat(one.getParent()).isSameAs(node); + assertThat(node.getChildByName("one") + .getChildByName("two") + .getChildByName("three") + .getChildByName("four")).isSameAs(four); + } + + @Test + @SuppressWarnings("unchecked") + public void canPlaceInTreeUnderExistingNode() { + //given + node = new NodeItem<>(null, "root"); + val child = new NodeItem("child data", "child"); + val grandchild = new NodeItem("grandchild data", "grandchild"); + //when + node.insertInPath(child); // as root/child + node.insertInPath(grandchild, "child"); // as root/child/grandchild + //then + assertThat(node.getChildByName("child")).as("child").isSameAs(child); + assertThat( + node.getChildByName("child").getChildByName("grandchild")).as( + "grandchild").isSameAs(grandchild); + } + + @Test + @SuppressWarnings("unchecked") + public void canPlaceInTreeAboveExistingNode() { + //given + node = new NodeItem<>(null, "root"); + val child = new NodeItem("child data", "child"); + val grandchild = new NodeItem("grandchild data", "grandchild"); + //when + node.insertInPath(grandchild, "child"); + node.insertInPath(child); + //then + assertThat(node.getChildByName("child").getData()).as("data in tree") + .isSameAs( + "child data"); + assertThat( + node.getChildByName("child").getChildByName("grandchild")).as( + "grandchild").isSameAs(grandchild); + } + + @Test + public void removingParentFromNodeWithNoParentIsNoop() { + //given + node = new NodeItem<>(null); + //when + node.removeParent(); + } + + @Test + public void removingParentFromNodeWithParentRemovesParent() { + //given + node = new NodeItem<>(null); + NodeItem child = new NodeItem<>(null, node); + //when + child.removeParent(); + //then + assertThat(child.getParent()).isNull(); + } + + @Test + public void placeNodeInTreeWhereNonEmptyNodeWithSameNameExists() { + //given + exception.expect(NodeException.class); + exception.expectMessage( + "A non-empty node with that name already exists here"); + node = new NodeItem<>(null); + val child = new NodeItem(null, "child", node); + new NodeItem<>("data", "grandchild", child); + // root -> child -> grandchild + // only grandchild has data + //when + // attempt to add another node called 'grandchild' to 'child' + node.insertInPath(new NodeItem<>("cuckoo", "grandchild"), "child"); + } + + @Test + @SuppressWarnings("unchecked") + public void placeNodeInTreeWhenAddedNodeIsUnnamed() { + //given + node = new NodeItem<>(null); + final Node newNode = new NodeItem<>(null); + //when + node.insertInPath(newNode); + //then + assertThat(node.getChildren()).containsOnly(newNode); + } + + @Test + @SuppressWarnings("unchecked") + public void placeNodeInTreeWhenEmptyChildWithTargetNameExists() { + //given + node = new NodeItem<>(null); + final NodeItem child = new NodeItem<>(null, "child"); + final NodeItem target = new NodeItem<>(null, "target"); + node.addChild(child); + child.addChild(target); + final NodeItem addMe = new NodeItem<>("I'm new", "target"); + assertThat(addMe.getParent()).isNull(); + assertThat(child.getChildByName("target").isEmpty()).as( + "target starts empty").isTrue(); + //when + // addMe should replace target as the sole descendant of child + node.insertInPath(addMe, "child"); + //then + assertThat(child.getChildByName("target").getData()).as( + "target now contains data").isEqualTo("I'm new"); + } + + @Test + public void findChildNamedShouldThrowNPEWhenNameIsNull() { + //given + exception.expect(NullPointerException.class); + exception.expectMessage("name"); + node = new NodeItem<>(null); + //when + node.findChildByName(null); + } + + @Test + public void isNamedNull() { + //given + node = new NodeItem<>(null); + //then + assertThat(node.isNamed()).isFalse(); + } + + @Test + public void isNamedEmpty() { + //given + node = new NodeItem<>(null, ""); + //then + assertThat(node.isNamed()).isFalse(); + } + + @Test + public void isNamedNamed() { + //given + node = new NodeItem<>(null, "named"); + //then + assertThat(node.isNamed()).isTrue(); + } + + @Test + public void removeParentNodeProvidesSameNameSupplier() { + // once a node has it's parent removed it should provide a default name + // provider + //given + node = new NodeItem<>("data", Node::getData); // name provider: getData + final NodeItem child = new NodeItem<>("other", node); + assertThat(node.getName()).as("initial root name").isEqualTo("data"); + assertThat(child.getName()).as("initial child name").isEqualTo("other"); + //when + child.removeParent(); + //then + assertThat(node.getName()).as("final root name").isEqualTo("data"); + assertThat(child.getName()).as("final child name").isEqualTo("other"); + } + + @Test + @SuppressWarnings("unchecked") + public void removeChildRemovesTheChild() { + //given + node = new NodeItem<>(null); + Node child = node.createChild("child"); + assertThat(node.getChildren()).containsExactly(child); + //then + node.removeChild(child); + //then + assertThat(node.getChildren()).isEmpty(); + } + + @Test + public void drawTreeIsCorrect() { + //given + node = new NodeItem<>(null, "root"); + val bob = new NodeItem(null, "bob", node); + val alice = new NodeItem(null, "alice", node); + new NodeItem<>(null, "dave", alice); + new NodeItem<>(null, bob); // has no name and no children so no included + val kim = new NodeItem(null, node); // nameless mother + new NodeItem<>(null, "lucy", kim); + //when + val tree = node.drawTree(0); + //then + String[] lines = tree.split("\n"); + assertThat(lines).contains("[root]", "[ alice]", "[ dave]", + "[ (unnamed)]", "[ lucy]", "[ bob]"); + assertThat(lines).containsSubsequence("[root]", "[ alice]", "[ dave]"); + assertThat(lines).containsSubsequence("[root]", "[ (unnamed)]", + "[ lucy]"); + assertThat(lines).containsSubsequence("[root]", "[ bob]"); + } + + @Test + public void canChangeNodeData() { + //given + node = new NodeItem<>("initial"); + //when + node.setData("updated"); + //then + assertThat(node.getData()).isEqualTo("updated"); + } + + @Test + @SuppressWarnings("unchecked") + public void canCreateNamedChild() { + //given + node = new NodeItem<>(null); + //when + Node child = node.createChild("child data", "child name"); + //then + assertThat(child.getName()).isEqualTo("child name"); + assertThat(child.getParent()).isSameAs(node); + assertThat(node.getChildren()).containsExactly(child); + } + + @Test + public void canGetChildWhenFound() { + //given + node = new NodeItem<>("data"); + Node child = new NodeItem<>("child data", "child name", node); + //when + Node found = node.getChild("child data"); + //then + assertThat(found).isSameAs(child); + } + + @Test + public void canGetChildWhenNotFound() { + //given + exception.expect(NodeException.class); + exception.expectMessage("Child not found"); + node = new NodeItem<>("data"); + //when + node.getChild("child data"); + } + + @Test + @SuppressWarnings("unchecked") + public void constructorWithNameSupplierAndParentBeChildOfParent() { + //given + node = new NodeItem<>(null); + //when + NodeItem child = new NodeItem<>(null, Node::getData, node); + //then + assertThat(child.getParent()).isSameAs(node); + assertThat(node.getChildren()).containsExactly(child); + } + + @Test + @SuppressWarnings("unchecked") + public void removeParentCopiesRootNameSupplier() { + //given + node = new NodeItem<>("root data", n -> "root supplier"); + val child = new NodeItem<>("child data", node); + assertThat(child.getName()).isEqualTo("root supplier"); + //when + child.removeParent(); + //then + assertThat(child.getName()).isEqualTo("root supplier"); + } + + @Test + @SuppressWarnings("unchecked") + public void removeParentDoesNotReplaceLocalNameSupplier() { + //given + node = new NodeItem<>("root data", n -> "root supplier"); + val child = new NodeItem<>("child data", n -> "local supplier", node); + assertThat(child.getName()).isEqualTo("local supplier"); + //when + child.removeParent(); + //then + assertThat(child.getName()).isEqualTo("local supplier"); + } + + @Test + public void setNameToNullRevertsToParentNameSupplier() { + //given + node = new NodeItem<>(null, n -> "root supplier"); + val child = new NodeItem(null, "child name", node); + assertThat(child.getName()).isEqualTo("child name"); + //when + child.setName(null); + //then + assertThat(child.getName()).isEqualTo("root supplier"); + } + + @Test + public void getNameWithNameSupplierIsRecalculatedEachCall() { + val counter = new AtomicInteger(0); + node = new NodeItem<>(null, + n -> Integer.toString(counter.incrementAndGet())); + //then + assertThat(node.getName()).isNotEqualTo(node.getName()); + } + + @Test + public void isNamedWithNameSupplierIsRecalculatedEachCall() { + val counter = new AtomicInteger(0); + node = new NodeItem<>(null, n -> { + // alternate between even numbers and nulls: null, 2, null, 4, null + final int i = counter.incrementAndGet(); + if (i % 2 == 0) { + return Integer.toString(i); + } + return null; + }); + //then + assertThat(node.isNamed()).isFalse(); + assertThat(node.isNamed()).isTrue(); + } + + @Test + public void canUseNameSupplierToBuildFullPath() { + //given + final Function, String> pathNameSupplier = node -> { + Node parent = node.getParent(); + if (parent == null) { + return ""; + } + return parent.getName() + "/" + node.getData(); + }; + node = new NodeItem<>(null, pathNameSupplier); + val child = new NodeItem("child", node); + val grandchild = new NodeItem("grandchild", child); + //then + assertThat(grandchild.getName()).isEqualTo("/child/grandchild"); + } }