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.kemitixnode
- 0.1.0
+ 0.2.0jarNode
@@ -15,6 +14,10 @@
0.6.0
+
+ 3.4.1
+
+
https://github.com/kemitix/node/issuesGitHub Issues
@@ -34,9 +37,9 @@
org.projectlomboklombok
- 1.16.6
+ 1.16.8
+ test
-
junitjunit
@@ -49,5 +52,11 @@
1.3test
+
+ 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");
+ }
}