diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /yolean |
Publish
Diffstat (limited to 'yolean')
37 files changed, 3008 insertions, 0 deletions
diff --git a/yolean/.gitignore b/yolean/.gitignore new file mode 100644 index 00000000000..12251442258 --- /dev/null +++ b/yolean/.gitignore @@ -0,0 +1,2 @@ +/target +/pom.xml.build diff --git a/yolean/OWNERS b/yolean/OWNERS new file mode 100644 index 00000000000..90fdb511ae3 --- /dev/null +++ b/yolean/OWNERS @@ -0,0 +1 @@ +bakksjo diff --git a/yolean/README.sh b/yolean/README.sh new file mode 100755 index 00000000000..af0cc99f589 --- /dev/null +++ b/yolean/README.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +if [ -z ${VERSION} ]; then + echo "Environment VERSION not set." >&2 + exit 1; +fi +cat <<EOF + +Yolean is a collection of Java utility classes that may be useful +across various products. INclusion here has a higher threshold than +vespajlib. + +EOF diff --git a/yolean/pom.xml b/yolean/pom.xml new file mode 100644 index 00000000000..c832f5e7f46 --- /dev/null +++ b/yolean/pom.xml @@ -0,0 +1,62 @@ +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + <artifactId>yolean</artifactId> + <version>6-SNAPSHOT</version> + <packaging>container-plugin</packaging> + <name>${project.artifactId}</name> + <dependencies> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-library</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>annotations</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>bundle-plugin</artifactId> + <extensions>true</extensions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <compilerArgs> + <arg>-Xlint:all</arg> + <arg>-Xlint:-serial</arg> + </compilerArgs> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/yolean/src/main/java/com/yahoo/yolean/Exceptions.java b/yolean/src/main/java/com/yahoo/yolean/Exceptions.java new file mode 100644 index 00000000000..51e8332809b --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/Exceptions.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean; + +/** + * Helper methods for handling exceptions + * + * @author bratseth + */ +public class Exceptions { + + /** + * <p>Returns a user friendly error message string which includes information from all nested exceptions.</p> + * + * <p>The form of this string is + * <code>e.getMessage(): e.getCause().getMessage(): e.getCause().getCause().getMessage()...</code> + * In addition, some heuristics are used to clean up common cases where exception nesting causes bad messages. + */ + public static String toMessageString(Throwable t) { + StringBuilder b = new StringBuilder(); + String lastMessage = null; + String message; + for (; t != null; t = t.getCause()) { + message = getMessage(t); + if (message == null) continue; + if (message.equals(lastMessage)) continue; + if (b.length() > 0) { + b.append(": "); + } + b.append(message); + lastMessage = message; + } + return b.toString(); + } + + /** Returns a useful message from *this* exception, or null if there is nothing useful to return */ + private static String getMessage(Throwable t) { + String message = t.getMessage(); + if (t.getCause() == null) { + if (message == null) return t.getClass().getSimpleName(); + } else { + if (message == null) return null; + if (message.equals(t.getCause().getClass().getName() + ": " + t.getCause().getMessage())) return null; + } + return message; + } + +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/After.java b/yolean/src/main/java/com/yahoo/yolean/chain/After.java new file mode 100644 index 00000000000..7664f30cc6b --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/After.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p>The component that is annotated with this must be placed later than the components or phases providing names + * contained in the given list.</p> + * + * @author tonytv + * @since 5.1.18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface After { + + public abstract String[] value() default { }; +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/Before.java b/yolean/src/main/java/com/yahoo/yolean/chain/Before.java new file mode 100644 index 00000000000..cd8c90be085 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/Before.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p>The component that is annotated with this must be placed earlier than the components or phases providing names + * contained in the given list.</p> + * + * @author tonytv + * @since 5.1.18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Before { + + public abstract String[] value() default { }; +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/Chain.java b/yolean/src/main/java/com/yahoo/yolean/chain/Chain.java new file mode 100644 index 00000000000..86ff0e07d00 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/Chain.java @@ -0,0 +1,98 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * An immutable and ordered list of components + * + * @author tonytv + */ +public final class Chain<T> implements Iterable<T> { + + private final String id; + private final Collection<T> components; + + @SafeVarargs + public Chain(String id, T... components) { + this(id, Arrays.asList(components)); + } + + public Chain(String id, List<? extends T> components) { + requireNonNull(id, "id must be non-null."); + requireNonNull(components, "components must be non-null"); + + this.components = ImmutableList.copyOf(components); + this.id = id; + } + + public String id() { + return id; + } + + public boolean isEmpty() { + return components.isEmpty(); + } + + @Override + public Iterator<T> iterator() { + return components.iterator(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("chain '").append(id).append("'{"); + boolean first = true; + for (T component : components) { + if (!first) { + b.append("->"); + } else { + first = false; + } + b.append(" ").append(component.getClass().getSimpleName()).append(" "); + } + b.append("}"); + return b.toString(); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other instanceof Chain && equals((Chain<?>)other); + } + + public boolean equals(Chain<?> other) { + return id.equals(other.id) && componentsIdentical(components, other.components); + } + + private boolean componentsIdentical(Collection<T> components1, Collection<?> components2) { + if (components1.size() != components2.size()) { + return false; + } + Iterator<T> iterator1 = components1.iterator(); + Iterator<?> iterator2 = components2.iterator(); + while (iterator1.hasNext()) { + T c1 = iterator1.next(); + Object c2 = iterator2.next(); + + if (c1 != c2) { + return false; + } + } + return true; + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/ChainBuilder.java b/yolean/src/main/java/com/yahoo/yolean/chain/ChainBuilder.java new file mode 100644 index 00000000000..ad44f2416d4 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/ChainBuilder.java @@ -0,0 +1,247 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author tonytv + * @author gjoranv + */ +public final class ChainBuilder<T> { + + private final String chainId; + private final List<T> components = new ArrayList<>(); + private final IdentityHashMap<T, Dependencies<T>> dependencies = new IdentityHashMap<>(); + + public ChainBuilder(String chainId) { + this.chainId = chainId; + } + + @SafeVarargs + public final ChainBuilder<T> add(T component, Dependencies<? extends T>... dependenciesList) { + if (dependencies.containsKey(component)) { + throw new IllegalArgumentException("The same component cannot be added twice: " + component); + } + components.add(component); + + List<Dependencies<? extends T>> allDependencies = + Dependencies.allOf(dependenciesList, Dependencies.getAnnotatedDependencies(component)); + dependencies.put(component, Dependencies.union(allDependencies)); + + return this; + } + + public Chain<T> build() { + DirectedGraph graph = buildGraph(); + List<Vertex> sortedVertices = graph.topologicalSort(); + return new Chain<>(chainId, components(sortedVertices)); + } + + private DirectedGraph buildGraph() { + DirectedGraph graph = new DirectedGraph(); + List<ComponentVertex<T>> vertices = createVertices(); + + addVertices(graph, vertices); + addEdges(graph, vertices, dependencies); + + return graph; + } + + private List<ComponentVertex<T>> createVertices() { + List<ComponentVertex<T>> vertices = new ArrayList<>(); + for (T component : components) { + vertices.add(new ComponentVertex<>(component)); + } + return vertices; + } + + @SuppressWarnings("unchecked") + private List<T> components(List<Vertex> sortedVertices) { + List<T> result = new ArrayList<>(); + for (Vertex vertex : sortedVertices) { + if (vertex instanceof ComponentVertex) { + result.add((T)((ComponentVertex)vertex).component); + } + } + return result; + } + + // TODO: create subclasses Beginning/EdingVertex instead? We could then create the correct class in createVertices, + // TODO: and call the same method in DirGraph for all types. + private void addVertices(DirectedGraph graph, List<ComponentVertex<T>> vertices) { + for (ComponentVertex<T> v : vertices) { + if (isBeginningVertex(v)) { + graph.addBeginningVertex(v); + } else if (isEndingVertex(v)) { + graph.addEndingVertex(v); + } else { + graph.addVertex(v); + } + } + } + + private boolean isBeginningVertex(ComponentVertex<T> v) { + return dependencies.get(v.component).before.providedNames.contains("*"); + } + + private boolean isEndingVertex(ComponentVertex<T> v) { + return dependencies.get(v.component).after.providedNames.contains("*"); + } + + private static <T> void addEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + addBeforeInstanceEdges(graph, vertices, dependencies); + addAfterInstanceEdges(graph, vertices, dependencies); + addBeforeClassEdges(graph, vertices, dependencies); + addAfterClassEdges(graph, vertices, dependencies); + addBeforeProvidedEdges(graph, vertices, dependencies); + addAfterProvidedEdges(graph, vertices, dependencies); + } + + // NOTE: When reading 'beforeVertex' below, think that 'vertex' should be _before_ beforeVertex. + + private static <T> void addBeforeClassEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + for (ComponentVertex<T> vertex : vertices) { + for (Class<? extends T> beforeClass : dependencies.get(vertex.component).before.classes) { + for (Vertex beforeVertex : componentsWithClass(vertices, beforeClass)) { + graph.addEdge(vertex, beforeVertex); + } + } + } + } + + private static <T> void addAfterClassEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + for (ComponentVertex<T> vertex : vertices) { + for (Class<? extends T> afterClass : dependencies.get(vertex.component).after.classes) { + for (Vertex afterVertex : componentsWithClass(vertices, afterClass)) { + graph.addEdge(afterVertex, vertex); + } + } + } + } + + private static <T> List<Vertex> componentsWithClass(List<ComponentVertex<T>> vertices, + Class<? extends T> beforeClass) { + List<Vertex> result = new ArrayList<>(); + for (ComponentVertex<T> vertex : vertices) { + if (beforeClass.isInstance(vertex.component)) { + result.add(vertex); + } + } + return result; + } + + private static <T> void addBeforeInstanceEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + IdentityHashMap<T, Vertex> componentToVertex = getComponentToVertex(vertices); + for (ComponentVertex<T> vertex : vertices) { + for (T before : dependencies.get(vertex.component).before.instances) { + Vertex beforeVertex = componentToVertex.get(before); + if (beforeVertex != null) { + graph.addEdge(vertex, beforeVertex); + } + } + } + } + + private static <T> void addAfterInstanceEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + IdentityHashMap<T, Vertex> componentToVertex = getComponentToVertex(vertices); + for (ComponentVertex<T> vertex : vertices) { + for (T after : dependencies.get(vertex.component).after.instances) { + Vertex afterVertex = componentToVertex.get(after); + if (afterVertex != null) { + graph.addEdge(afterVertex, vertex); + } + } + } + } + + private static <T> IdentityHashMap<T, Vertex> getComponentToVertex(List<ComponentVertex<T>> vertices) { + IdentityHashMap<T, Vertex> result = new IdentityHashMap<>(); + for (ComponentVertex<T> vertex : vertices) { + result.put(vertex.component, vertex); + } + return result; + } + + private static <T> void addBeforeProvidedEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + Map<String, Set<Vertex>> providedNames = getProvidedNames(vertices, dependencies); + for (ComponentVertex<T> vertex : vertices) { + for (String name : dependencies.get(vertex.component).before.providedNames) { + for (Vertex beforeVertex : emptyIfNull(providedNames.get(name))) { + graph.addEdge(vertex, beforeVertex); + } + } + } + } + + private static <T> void addAfterProvidedEdges(DirectedGraph graph, + List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + Map<String, Set<Vertex>> providedNames = getProvidedNames(vertices, dependencies); + for (ComponentVertex<T> vertex : vertices) { + for (String name : dependencies.get(vertex.component).after.providedNames) { + for (Vertex afterVertex : emptyIfNull(providedNames.get(name))) { + graph.addEdge(afterVertex, vertex); + } + } + } + } + + private static <T> Map<String, Set<Vertex>> getProvidedNames(List<ComponentVertex<T>> vertices, + IdentityHashMap<T, Dependencies<T>> dependencies) { + Map<String, Set<Vertex>> result = new HashMap<>(); + for (ComponentVertex<T> vertex : vertices) { + for (String providedName : dependencies.get(vertex.component).provided) { + getIdentitySet(result, providedName).add(vertex); + } + addClassName(result, vertex); + } + return result; + } + + private static void addClassName(Map<String, Set<Vertex>> providedNamesToVertex, ComponentVertex vertex) { + String className = vertex.component.getClass().getName(); + getIdentitySet(providedNamesToVertex, className).add(vertex); + } + + private static <T> Set<T> getIdentitySet(Map<String, Set<T>> map, String key) { + Set<T> result = map.get(key); + if (result == null) { + result = Collections.newSetFromMap(new IdentityHashMap<T, Boolean>()); + map.put(key, result); + } + return result; + } + + private static <T> Set<T> emptyIfNull(Set<T> set) { + return set != null ? + set : + Collections.<T>emptySet(); + } + + private static class ComponentVertex<T> implements Vertex { + + final T component; + + private ComponentVertex(T component) { + this.component = component; + } + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/ChainCycleException.java b/yolean/src/main/java/com/yahoo/yolean/chain/ChainCycleException.java new file mode 100644 index 00000000000..b92df20ab8c --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/ChainCycleException.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author tonytv + */ +public class ChainCycleException extends RuntimeException { + + private final List<?> components; + + public ChainCycleException(List<?> components) { + this.components = ImmutableList.copyOf(components); + } + + public List<?> components() { + return components; + } +}
\ No newline at end of file diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/Dependencies.java b/yolean/src/main/java/com/yahoo/yolean/chain/Dependencies.java new file mode 100644 index 00000000000..ca59c373351 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/Dependencies.java @@ -0,0 +1,191 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author tonytv + * @author gjoranv + */ +public class Dependencies<T> { + + final Order<T> before; + final Order<T> after; + final List<String> provided; + + private Dependencies(Order<T> before, Order<T> after, String[] provided) { + this.before = before; + this.after = after; + this.provided = copyList(provided); + } + + @SafeVarargs + public static <T> Dependencies<T> before(T... components) { + return new Dependencies<>(new Order<>(components, null, null), Order.<T>emptyOrder(), null); + } + + @SafeVarargs + public static <T> Dependencies<T> before(Class<? extends T>... classes) { + return new Dependencies<>(new Order<>(null, classes, null), Order.<T>emptyOrder(), null); + } + + @SuppressWarnings("unchecked") + public static Dependencies before(String... providedNames) { + // Does not use type parameters due to Javas limited type inference. + return new Dependencies(new Order(null, null, providedNames), Order.emptyOrder(), null); + } + + @SafeVarargs + public static <T> Dependencies<T> after(T... components) { + return new Dependencies<>(Order.<T>emptyOrder(), new Order<>(components, null, null), null); + } + + @SafeVarargs + public static <T> Dependencies<T> after(Class<? extends T>... classes) { + return new Dependencies<>(Order.<T>emptyOrder(), new Order<>(null, classes, null), null); + } + + @SuppressWarnings("unchecked") + public static Dependencies after(String... providedNames) { + // Does not use type parameters due to Javas limited type inference. + return new Dependencies(Order.emptyOrder(), new Order(null, null, providedNames), null); + } + + @SuppressWarnings("unchecked") + public static Dependencies provides(String... names) { + // Does not use type parameters due to Javas limited type inference. + return new Dependencies(Order.emptyOrder(), Order.emptyOrder(), names); + } + + public static <T> Dependencies<T> emptyDependencies() { + return new Dependencies<>(Order.<T>emptyOrder(), Order.<T>emptyOrder(), null); + } + + @SuppressWarnings("unchecked") + static <T> Dependencies<T> union(List<Dependencies<? extends T>> dependenciesList) { + if (dependenciesList.size() > 1) { + Dependencies<T> result = emptyDependencies(); + for (Dependencies<? extends T> dependencies : dependenciesList) { + result = result.union(dependencies); + } + return result; + } else if (dependenciesList.size() == 0) { + return emptyDependencies(); + } else { + return (Dependencies<T>)dependenciesList.get(0); // Dependencies<T> is covariant for T, the cast is valid. + } + } + + private Dependencies<T> union(Dependencies<? extends T> other) { + List<String> lst = listUnion(provided, other.provided); + return new Dependencies<>(before.union(other.before), + after.union(other.after), + lst.toArray(new String[lst.size()])); + } + + private static <T> List<T> listUnion(List<? extends T> list1, List<? extends T> list2) { + List<T> union = new ArrayList<>(list1); + union.removeAll(list2); + union.addAll(list2); + return union; + } + + static <T> Dependencies<T> getAnnotatedDependencies(T component) { + return new Dependencies<>( + new Order<T>(null, null, getSymbols(component, Before.class)), + new Order<T>(null, null, getSymbols(component, After.class)), + getProvidedSymbols(component)); + } + + private static <T> String[] getProvidedSymbols(T component) { + List<String> lst = allOf(getSymbols(component, Provides.class), component.getClass().getName()); + return lst.toArray(new String[lst.size()]); + } + + @SafeVarargs + static <T> List<T> allOf(List<T> elements, T... otherElements) { + List<T> result = new ArrayList<>(elements); + result.addAll(Arrays.asList(otherElements)); + return result; + } + + @SafeVarargs + static <T> List<T> allOf(T[] elements, T... otherElements) { + return allOf(Arrays.asList(elements), otherElements); + } + + private static <T> List<String> getSymbols(T component, Class<? extends Annotation> annotationClass) { + List<String> result = new ArrayList<>(); + + result.addAll(annotationSymbols(component, annotationClass)); + return result; + } + + private static <T> Collection<String> annotationSymbols(T component, Class<? extends Annotation> annotationClass) { + try { + List<String> values = new ArrayList<>(); + + Class clazz = component.getClass(); + while (clazz != null) { + Annotation annotation = clazz.getAnnotation(annotationClass); + if (annotation != null) { + values.addAll(Arrays.asList((String[])annotationClass.getMethod("value").invoke(annotation))); + } + clazz = clazz.getSuperclass(); + } + return values; + } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static <U> List<U> copyList(List<U> list) { + return list == null ? + Collections.<U>emptyList() : + new ArrayList<>(list); + } + + private static <U> List<U> copyList(U[] array) { + return array == null ? + Collections.<U>emptyList() : + new ArrayList<>(Arrays.<U>asList(array)); + } + + static final class Order<T> { + + final List<T> instances; + final List<Class<? extends T>> classes; + final List<String> providedNames; + + private Order(T[] instances, Class<? extends T>[] classes, String[] providedNames) { + this.instances = copyList(instances); + this.classes = copyList(classes); + this.providedNames = copyList(providedNames); + } + + private Order(List<T> instances, List<Class<? extends T>> classes, List<String> providedNames) { + this.instances = copyList(instances); + this.classes = copyList(classes); + this.providedNames = copyList(providedNames); + } + + // TODO: unit test + private Order<T> union(Order<? extends T> other) { + return new Order<>( + listUnion(instances, other.instances), + listUnion(classes, other.classes), + listUnion(providedNames, other.providedNames)); + } + + // TODO: try to make it possible to use 'null' Order in Dependencies instead. + private static <U> Order<U> emptyOrder() { + return new Order<>((U[])null, null, null); + } + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/DirectedGraph.java b/yolean/src/main/java/com/yahoo/yolean/chain/DirectedGraph.java new file mode 100644 index 00000000000..260ffe33b5f --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/DirectedGraph.java @@ -0,0 +1,106 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * TODO: prioritize vertices in edge map. + * + * @author tonytv + */ +class DirectedGraph { + + private IdentityHashMap<Vertex, List<Vertex>> incommingEdges = new IdentityHashMap<>(); + private List<Vertex> vertices = new ArrayList<>(); + private List<Vertex> beginningVertices = new ArrayList<>(); + private List<Vertex> endingVertices = new ArrayList<>(); + + public void addVertex(Vertex vertex) { + vertices.add(vertex); + } + + public void addBeginningVertex(Vertex vertex) { + beginningVertices.add(vertex); + } + + public void addEndingVertex(Vertex vertex) { + endingVertices.add(vertex); + } + + public void addEdge(Vertex start, Vertex end) { + get(incommingEdges, end).add(start); + } + + private static List<Vertex> get(Map<Vertex, List<Vertex>> edgeMap, Vertex key) { + List<Vertex> value = edgeMap.get(key); + return value == null ? + addEmptyList(edgeMap, key) : + value; + } + + private static List<Vertex> addEmptyList(Map<Vertex, List<Vertex>> edgeMap, Vertex key) { + List<Vertex> value = new ArrayList<>(); + edgeMap.put(key, value); + return value; + } + + public List<Vertex> topologicalSort() { + EnumeratedIdentitySet<Vertex> visitedVertices = new EnumeratedIdentitySet<>(); + + for (Vertex v : beginningVertices) { + topologicalSortVisit(v, visitedVertices); + } + + warnIfVisitedEndVertices(visitedVertices); + + for (Vertex v : vertices) { + topologicalSortVisit(v, visitedVertices); + } + + // TODO: review this! + for (Vertex v : endingVertices) { + topologicalSortVisit(v, visitedVertices); + } + + return visitedVertices.insertionOrderedList(); + } + + private void warnIfVisitedEndVertices(EnumeratedIdentitySet<Vertex> visitedVertices) { + //TVT: + } + + private void topologicalSortVisit(Vertex vertex, Set<Vertex> visitedVertices) { + topologicalSortVisitImpl(vertex, visitedVertices, new EnumeratedIdentitySet<Vertex>()); + } + + private void topologicalSortVisitImpl(Vertex vertex, Set<Vertex> visitedVertices, + EnumeratedIdentitySet<Vertex> cycleDetector) { + if (cycleDetector.contains(vertex)) { + throw new ChainCycleException(cycleDetector.insertionOrderedList()); + } + + if (visitedVertices.contains(vertex)) { + return; + } + + cycleDetector.add(vertex); + + for (Vertex endVertex : emptyIfNull(incommingEdges.get(vertex))) { + topologicalSortVisitImpl(endVertex, visitedVertices, cycleDetector); + } + + visitedVertices.add(vertex); + cycleDetector.remove(vertex); + } + + private <T> List<T> emptyIfNull(List<T> list) { + return list == null ? + Collections.<T>emptyList() : + list; + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/EnumeratedIdentitySet.java b/yolean/src/main/java/com/yahoo/yolean/chain/EnumeratedIdentitySet.java new file mode 100644 index 00000000000..902391e7d24 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/EnumeratedIdentitySet.java @@ -0,0 +1,183 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * A set using identity comparison. + * Keeps track of insertion order, which is available by calling insertionOrderedList. + * + * @author tonytv + */ +class EnumeratedIdentitySet<T> implements Set<T> { + + private int counter = 0; + private final Map<T, Integer> set = new IdentityHashMap<>(); + + public EnumeratedIdentitySet(Collection<? extends T> collection) { + addAll(collection); + } + + public EnumeratedIdentitySet() { + // empty + } + + @Override + public int size() { + return set.size(); + } + + @Override + public boolean isEmpty() { + return set.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return set.containsKey(o); + } + + @Override + public Iterator<T> iterator() { + return set.keySet().iterator(); + } + + @Override + public Object[] toArray() { + return set.keySet().toArray(); + } + + @Override + public <T1> T1[] toArray(T1[] a) { + return set.keySet().toArray(a); + } + + @Override + public boolean add(T t) { + if (set.containsKey(t)) { + return false; + } else { + set.put(t, counter++); + return true; + } + } + + @Override + public boolean remove(Object o) { + return set.remove(o) != null; + } + + @Override + public boolean containsAll(Collection<?> c) { + return set.keySet().containsAll(c); + } + + @Override + public boolean addAll(Collection<? extends T> collection) { + boolean changed = false; + + for (T t : collection) { + changed |= add(t); + } + + return changed; + } + + @Override + public boolean retainAll(Collection<?> collection) { + return set.keySet().retainAll(collection); + } + + @Override + public boolean removeAll(Collection<?> collection) { + boolean changed = false; + + for (Object o : collection) { + changed |= remove(o); + } + + return changed; + } + + @Override + public void clear() { + set.clear(); + counter = 0; + } + + public List<T> insertionOrderedList() { + if (set.isEmpty()) { + counter = 0; + return Collections.emptyList(); + } + + if (counter >= set.size() * 2 + 20) { + renumber(); + } + + return getKeysSortedByValue(set, counter); + } + + private static <KEY> List<KEY> getKeysSortedByValue(Map<KEY, Integer> set, int maxValue) { + @SuppressWarnings("unchecked") + KEY[] result = (KEY[])Array.newInstance(headKey(set).getClass(), maxValue); + + for (Map.Entry<KEY, Integer> entry : set.entrySet()) { + result[entry.getValue()] = entry.getKey(); + } + + return removeNulls(result); + } + + private static <T> T headKey(Map<T, ?> map) { + return map.entrySet().iterator().next().getKey(); + } + + static <T> List<T> removeNulls(T[] list) { + int insertionSpot = 0; + for (int i = 0; i < list.length; i++) { + T element = list[i]; + if (element != null) { + list[insertionSpot] = element; + insertionSpot++; + } + } + return Arrays.asList(list).subList(0, insertionSpot); + } + + //only for testing + List<Integer> numbers() { + return new ArrayList<>(set.values()); + } + + private void renumber() { + SortedMap<Integer, T> invertedSet = invertedSortedMap(set); + + int i = 0; + for (Map.Entry<Integer, T> entry : invertedSet.entrySet()) { + set.put(entry.getValue(), i++); + } + counter = i; + } + + private static <K, V> SortedMap<V, K> invertedSortedMap(Map<K, V> map) { + SortedMap<V, K> result = new TreeMap<>(); + + for (Map.Entry<K, V> entry : map.entrySet()) { + result.put(entry.getValue(), entry.getKey()); + } + + return result; + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/Provides.java b/yolean/src/main/java/com/yahoo/yolean/chain/Provides.java new file mode 100644 index 00000000000..99cfaf1fef2 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/Provides.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <p>Mark this component as providing some named functionality. Other components can then mark themselves as "before" + * and "after" the string provided here, to impose constraints on ordering.</p> + * + * @author tonytv + * @since 5.1.18 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Provides { + + public abstract String[] value() default { }; +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/Vertex.java b/yolean/src/main/java/com/yahoo/yolean/chain/Vertex.java new file mode 100644 index 00000000000..57de9c3d81c --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/Vertex.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +/** + * @author tonytv + */ +interface Vertex { + +} diff --git a/yolean/src/main/java/com/yahoo/yolean/chain/package-info.java b/yolean/src/main/java/com/yahoo/yolean/chain/package-info.java new file mode 100644 index 00000000000..a17b39169c7 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/chain/package-info.java @@ -0,0 +1,6 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi package com.yahoo.yolean.chain; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/yolean/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java b/yolean/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java new file mode 100644 index 00000000000..38c6abf055b --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMap.java @@ -0,0 +1,94 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * <p>This is a thread hash map for small collections that are stable once built. Until it is stable there will be a + * race among all threads missing something in the map. They will then clone the map add the missing stuff and then put + * it back as active again. Here are no locks, but the cost is that inserts will happen a lot more than necessary. The + * map reference is volatile, but on most multi-cpu machines that has no cost unless modified.</p> + * + * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a> + * @since 5.2 + */ +public class CopyOnWriteHashMap<K, V> implements Map<K, V> { + + private volatile HashMap<K, V> map = new HashMap<>(); + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public V get(Object key) { + return map.get(key); + } + + @Override + public V put(K key, V value) { + HashMap<K, V> next = new HashMap<>(map); + V old = next.put(key, value); + map = next; + return old; + } + + @Override + @SuppressWarnings("SuspiciousMethodCalls") + public V remove(Object key) { + HashMap<K, V> prev = map; + if (!prev.containsKey(key)) { + return null; + } + HashMap<K, V> next = new HashMap<>(prev); + V old = next.remove(key); + map = next; + return old; + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + HashMap<K, V> next = new HashMap<>(map); + next.putAll(m); + map = next; + } + + @Override + public void clear() { + map = new HashMap<>(); + } + + @Override + public Set<K> keySet() { + return map.keySet(); + } + + @Override + public Collection<V> values() { + return map.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return map.entrySet(); + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java b/yolean/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java new file mode 100644 index 00000000000..5060ed8ef6a --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/concurrent/ThreadRobustList.java @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * <p>This class implements a thread-safe, lock-free list of Objects that supports multiple readers and a single writer. + * Because there are no locks or other memory barriers involved, there exists no <i>happens-before</i> relationship + * among calls to either methods of the <tt>ThreadRobustList</tt>. This means that there are no guarantees as to when + * (or even if) an item {@link #add(Object)}ed becomes visible through {@link #iterator()}. If visibility is required, + * either use explicit synchronization between reader and writer thread, or move to a different concurrent collection + * (e.g. <tt>CopyOnWriteArrayList</tt>).</p> + * <p>Because it is lock-free, the <tt>ThreadRobustList</tt> has minimal overhead to both reading and writing. The + * iterator offered by this class always observes the list in a consistent state, and it never throws a + * <tt>ConcurrentModificationException</tt>.</p> + * <p>The <tt>ThreadRobustList</tt> does not permit adding <tt>null</tt> items.</p> + * <p>The usage of <tt>ThreadRobustList</tt> has no memory consistency effects. </p> + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + * @since 5.1.15 + */ +public class ThreadRobustList<T> implements Iterable<T> { + + private Object[] items; + private int next = 0; + + /** + * <p>Constructs a new instance of this class with an initial capacity of <tt>10</tt>.</p> + */ + public ThreadRobustList() { + this(10); + } + + /** + * <p>Constructs a new instance of this class with a given initial capacity.</p> + * + * @param initialCapacity the initial capacity of this list + */ + public ThreadRobustList(int initialCapacity) { + items = new Object[initialCapacity]; + } + + /** + * <p>Returns whether or not this list is empty.</p> + * + * @return <tt>true</tt> if this list has zero items + */ + public boolean isEmpty() { + return next == 0; + } + + /** + * <p>Adds an item to this list. As opposed to <tt>CopyOnWriteArrayList</tt>, items added to this list may become + * visible to iterators created <em>before</em> a call to this method.</p> + * + * @param item the item to add + * @throws NullPointerException if <tt>item</tt> is <tt>null</tt> + */ + public void add(T item) { + if (item == null) { + throw new NullPointerException(); + } + Object[] workItems = items; + if (next >= items.length) { + workItems = Arrays.copyOf(workItems, 20 + items.length * 2); + workItems[next++] = item; + items = workItems; + } else { + workItems[next++] = item; + } + } + + /** + * <p>Returns an iterator over the items in this list. As opposed to <tt>CopyOnWriteArrayList</tt>, this iterator + * may see items added to the <tt>ThreadRobustList</tt> even if they occur <em>after</em> a call to this method.</p> + * <p>The returned iterator does not support <tt>remove()</tt>.</p> + * + * @return an iterator over this list + */ + @Override + public Iterator<T> iterator() { + return new ThreadRobustIterator<>(items); + } + + private static class ThreadRobustIterator<T> implements Iterator<T> { + + final Object[] items; + int nextIndex = 0; + + ThreadRobustIterator(Object[] items) { + this.items = items; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return (T)items[nextIndex++]; + } + + @Override + public boolean hasNext() { + if (nextIndex >= items.length) { + return false; + } + if (items[nextIndex] == null) { + return false; + } + return true; + } + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/concurrent/package-info.java b/yolean/src/main/java/com/yahoo/yolean/concurrent/package-info.java new file mode 100644 index 00000000000..2062e224f79 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/concurrent/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean.concurrent; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/yolean/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java new file mode 100644 index 00000000000..1ba28d27c4c --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingConsumer.java @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +import java.util.Objects; + +/** + * Functional interface that mirrors the Consumer interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingConsumer<T, E extends Throwable> { + void accept(T input) throws E; + + default ThrowingConsumer<T, E> andThen(ThrowingConsumer<? super T, ? extends E> after) { + Objects.requireNonNull(after); + return (T t) -> { accept(t); after.accept(t); }; + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java new file mode 100644 index 00000000000..3db84c6ba95 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingFunction.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +import java.util.Objects; + +/** + * Functional interface that mirrors the Function interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingFunction<T, R, E extends Throwable> { + R apply(T input) throws E; + + default <V> ThrowingFunction<T, V, E> andThen(ThrowingFunction<? super R, ? extends V, ? extends E> after) { + Objects.requireNonNull(after); + return (T t) -> after.apply(apply(t)); + } + + default <V> ThrowingFunction<V, R, E> compose(ThrowingFunction<? super V, ? extends T, ? extends E> before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java new file mode 100644 index 00000000000..bee32af563b --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/function/ThrowingSupplier.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.function; + +/** + * Functional interface that mirrors the Supplier interface, but allows for an + * exception to be thrown. + * + * @author oyving + */ +@FunctionalInterface +public interface ThrowingSupplier<R, E extends Throwable> { + R get() throws E; +} diff --git a/yolean/src/main/java/com/yahoo/yolean/function/package-info.java b/yolean/src/main/java/com/yahoo/yolean/function/package-info.java new file mode 100644 index 00000000000..38f7effc2bb --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/function/package-info.java @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author oyving + */ +@ExportPackage +@PublicApi +package com.yahoo.yolean.function; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/yolean/src/main/java/com/yahoo/yolean/package-info.java b/yolean/src/main/java/com/yahoo/yolean/package-info.java new file mode 100644 index 00000000000..444ed2496be --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/yolean/src/main/java/com/yahoo/yolean/trace/TraceNode.java b/yolean/src/main/java/com/yahoo/yolean/trace/TraceNode.java new file mode 100644 index 00000000000..d7e9d128ebd --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/trace/TraceNode.java @@ -0,0 +1,247 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import com.yahoo.yolean.concurrent.ThreadRobustList; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * <p>This class represents a single node in a tree of <tt>TraceNodes</tt>. The trace forms a tree where there is a + * branch for each parallel execution, and a node within such a branch for each traced event. As each <tt>TraceNode</tt> + * may contain a payload of any type, the trace tree can be used to exchange any thread-safe state between producers and + * consumers in different threads, whether or not the shape of the trace tree is relevant to the particular + * information.</p> + * <p>This class uses a {@link ThreadRobustList} for its children. That list allows multiple threads to inspect the + * hierarchy of a <tt>TraceNode</tt> tree while there are other threads concurrently modifying it, without incurring the + * cost of memory synchronization. The only caveat being that for each <tt>TraceNode</tt> there can never be more than + * exactly one writer thread. If multiple threads need to mutate a single <tt>TraceNode</tt>, then the writer threads + * need to synchronize their access on the <tt>TraceNode</tt>.</p> + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + * @since 5.1.15 + */ +public class TraceNode { + + private final Object payload; + private final long timestamp; + private ThreadRobustList<TraceNode> children; + private TraceNode parent; + + /** + * <p>Creates a new instance of this class.</p> + * + * @param payload the payload to assign to this, may be <tt>null</tt> + * @param timestamp the timestamp to assign to this + */ + public TraceNode(Object payload, long timestamp) { + this.payload = payload; + this.timestamp = timestamp; + } + + /** + * <p>Adds another <tt>TraceNode</tt> as a child to this.</p> + * + * @param child the TraceNode to add + * @return this, to allow chaining + * @throws IllegalArgumentException if <tt>child</tt> is not a root TraceNode + * @see #isRoot() + */ + public TraceNode add(TraceNode child) { + if (child.parent != null) { + throw new IllegalArgumentException("Can not add " + child + " to " + this + "; it is not a root."); + } + child.parent = this; + if (children == null) { + children = new ThreadRobustList<>(); + } + children.add(child); + return this; + } + + /** + * <p>Returns a read-only iterable of all {@link #payload() payloads} that are instances of <tt>payloadType</tt>, + * in all its decendants. The payload of <em>this</em> <tt>TraceNode</tt> is ignored.</p> + * <p>The payloads are retrieved in depth-first, prefix order.</p> + * + * @param payloadType the type of payloads to retrieve + * @return the payloads, never <tt>null</tt> + */ + public <PAYLOADTYPE> Iterable<PAYLOADTYPE> descendants(final Class<PAYLOADTYPE> payloadType) { + if (children == null) { + return Collections.emptyList(); + } + return new Iterable<PAYLOADTYPE>() { + + @Override + public Iterator<PAYLOADTYPE> iterator() { + return new PayloadIterator<>(TraceNode.this, payloadType); + } + }; + } + + /** + * <p>Returns the payload of this <tt>TraceNode</tt>, or null if none.</p> + * + * @return the payload + */ + public Object payload() { + return payload; + } + + /** + * <p>Returns the timestamp of this <tt>TraceNode</tt>.</p> + * + * @return the timestamp + */ + public long timestamp() { + return timestamp; + } + + /** + * <p>Returns the parent <tt>TraceNode</tt> of this.</p> + * + * @return the parent + */ + public TraceNode parent() { + return parent; + } + + /** + * <p>Returns the child <tt>TraceNodes</tt> of this.</p> + * + * @return the children + */ + public Iterable<TraceNode> children() { + if (children == null) { + return Collections.emptyList(); + } + return children; + } + + /** + * <p>Returns whether or not this <tt>TraceNode</tt> is a root node (i.e. it has no parent).</p> + * + * @return <tt>true</tt> if {@link #parent()} returns <tt>null</tt> + */ + public boolean isRoot() { + return parent == null; + } + + /** + * <p>Returns the root <tt>TraceNode</tt> of the tree that this <tt>TraceNode</tt> belongs to.</p> + * + * @return the root + */ + public TraceNode root() { + TraceNode node = this; + while (node.parent != null) { + node = node.parent; + } + return node; + } + + /** + * <p>Visits this <tt>TraceNode</tt> and all of its descendants in depth-first, prefix order.</p> + * + * @param visitor The visitor to accept. + * @return The <code>visitor</code> parameter. + */ + public <T extends TraceVisitor> T accept(T visitor) { + visitor.visit(this); + if (children == null || children.isEmpty()) { + return visitor; + } + visitor.entering(this); + for (TraceNode child : children) { + child.accept(visitor); + } + visitor.leaving(this); + return visitor; + } + + @Override + public String toString() { + final StringBuilder out = new StringBuilder("[ "); + accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + if (node.payload != null) { + out.append(node.payload).append(" "); + } + } + + @Override + public void entering(TraceNode node) { + out.append("[ "); + } + + @Override + public void leaving(TraceNode node) { + out.append("] "); + } + }); + return out.append("]").toString(); + } + + private static class PayloadIterator<PAYLOADTYPE> implements Iterator<PAYLOADTYPE> { + + final List<TraceNode> unexploredNodes = new LinkedList<>(); + final Class<PAYLOADTYPE> payloadType; + PAYLOADTYPE next; + + PayloadIterator(TraceNode root, Class<PAYLOADTYPE> payloadType) { + payloadType.getClass(); // throws NullPointerException + this.payloadType = payloadType; + unexploredNodes.add(root); + next = advance(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public PAYLOADTYPE next() { + if (next == null) { + throw new NoSuchElementException(); + } + PAYLOADTYPE current = next; + next = advance(); + return current; + } + + PAYLOADTYPE advance() { + // Current node is depleted, find next + while (unexploredNodes.size() > 0) { + // Take the next node + TraceNode node = unexploredNodes.remove(0); + + // Add its children to the list of nodes we1'll look at + if (node.children != null) { + int i = 0; // used to fabricate depth-first traversal order + for (TraceNode child : node.children) { + unexploredNodes.add(i++, child); + } + } + + Object payload = node.payload(); + if (payloadType.isInstance(payload)) { + return payloadType.cast(payload); + } + } + return null; + } + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java b/yolean/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java new file mode 100644 index 00000000000..2cb48616e56 --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/trace/TraceVisitor.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +/** + * <p>This class is an abstract visitor of {@link TraceNode}. See {@link TraceNode#accept(TraceVisitor)}.</p> + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + * @since 5.1.15 + */ +public abstract class TraceVisitor { + + /** + * <p>Visits a {@link TraceNode}. Called by {@link TraceNode#accept(TraceVisitor)}, before visiting its + * children.</p> + * + * @param node the <tt>TraceNode</tt> being visited + * @see TraceNode#accept(TraceVisitor) + */ + public abstract void visit(TraceNode node); + + /** + * <p>Enters a {@link TraceNode}. This method is called after {@link #visit(TraceNode)}, but before visiting its + * children. Note that this method is NOT called if a <tt>TraceNode</tt> has zero children.</p> + * <p>The default implementation of this method does nothing.</p> + * + * @param node the <tt>TraceNode</tt> being entered + * @see #leaving(TraceNode) + */ + @SuppressWarnings("UnusedParameters") + public void entering(TraceNode node) { + // empty + } + + /** + * <p>Leaves a {@link TraceNode}. This method is called after {@link #entering(TraceNode)}, and after visiting its + * children. Note that this method is NOT called if a <tt>TraceNode</tt> has zero children.</p> + * <p>The default implementation of this method does nothing.</p> + * + * @param node the <tt>TraceNode</tt> being left + */ + @SuppressWarnings("UnusedParameters") + public void leaving(TraceNode node) { + // empty + } +} diff --git a/yolean/src/main/java/com/yahoo/yolean/trace/package-info.java b/yolean/src/main/java/com/yahoo/yolean/trace/package-info.java new file mode 100644 index 00000000000..7032992628c --- /dev/null +++ b/yolean/src/main/java/com/yahoo/yolean/trace/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.yolean.trace; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/yolean/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java b/yolean/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java new file mode 100644 index 00000000000..36c295ba7f4 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/ExceptionsTestCase.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean; + +/** + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class ExceptionsTestCase extends junit.framework.TestCase { + + public ExceptionsTestCase(String name) { + super(name); + } + + public void testToMessageStrings() { + assertEquals("Blah",Exceptions.toMessageString(new Exception("Blah"))); + assertEquals("Blah", Exceptions.toMessageString(new Exception(new Exception("Blah")))); + assertEquals("Exception",Exceptions.toMessageString(new Exception())); + assertEquals("Foo: Blah",Exceptions.toMessageString(new Exception("Foo",new Exception(new IllegalArgumentException("Blah"))))); + assertEquals("Foo",Exceptions.toMessageString(new Exception("Foo",new Exception("Foo")))); + assertEquals("Foo: Exception",Exceptions.toMessageString(new Exception("Foo",new Exception()))); + assertEquals("Foo",Exceptions.toMessageString(new Exception(new Exception("Foo")))); + } + +} diff --git a/yolean/src/test/java/com/yahoo/yolean/chain/ChainBuilderTest.java b/yolean/src/test/java/com/yahoo/yolean/chain/ChainBuilderTest.java new file mode 100644 index 00000000000..86dba99e333 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/chain/ChainBuilderTest.java @@ -0,0 +1,394 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.yahoo.yolean.chain.Dependencies.after; +import static com.yahoo.yolean.chain.Dependencies.before; +import static com.yahoo.yolean.chain.Dependencies.provides; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.Assert.assertTrue; + +/** + * @author tonytv + * @author gjoranv + */ +public class ChainBuilderTest { + + static class Filter { + + } + + static class FilterA extends Filter { + + } + + static class FilterB extends Filter { + + } + + static class FilterExtendsA extends FilterA { + + } + + static class FilterExtendsB extends FilterB { + + } + + @Provides("A") + static class ProvidesA extends Filter { + + } + + @Provides("B") + static class ProvidesB extends Filter { + + } + + @Before("A") + static class BeforeA extends Filter { + + } + + @After("A") + static class AfterA extends Filter { + + } + + @Before("*") + @Provides("BeforeAll") + static class BeforeAll extends Filter { + + } + + @After("*") + @Provides("AfterAll") + static class AfterAll extends Filter { + + } + + @Before({ "BeforeAll", "*" }) + static class BeforeBeforeAll extends Filter { + + } + + @After({ "AfterAll", "*" }) + static class AfterAfterAll extends Filter { + + } + + static class ExtendsProvidesA extends ProvidesA { + + } + + @Provides("ExtendsA") + static class ProvidesA_and_ProvidesExtendsA extends ProvidesA { + + } + + @Before("B") + static class BeforeA_and_BeforeB extends BeforeA { + + } + + @Test + public void build_empty_chain() { + Chain<Filter> chain = getChain().build(); + assertTrue(chain.isEmpty()); + assertThat(chain.id(), is("myChain")); + } + + @Test + public void filters_without_dependencies_are_not_reordered() { + List<Filter> filters = new ArrayList<>(); + + ChainBuilder<Filter> chain = new ChainBuilder<>("myChain"); + for (int i = 0; i < 10; ++i) { + Filter filter = new Filter(); + filters.add(filter); + chain.add(filter); + } + + assertThat(chain.build(), contains(filters.toArray())); + } + + @Test(expected = ChainCycleException.class) + public void cycles_are_detected() { + Filter a = new Filter(); + Filter b = new Filter(); + + getChain().add(b, before(a)).add(a, before(b)).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void adding_same_instance_twice_is_illegal() { + Filter a = new Filter(); + + getChain().add(a).add(a).build(); + } + + @Test + public void before_instance() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b).add(a, before(b)).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void after_instance() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b, after(a)).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void before_class() { + Filter a = new FilterA(); + Filter b = new FilterB(); + + Chain<Filter> chain = getChain(). + add(b).add(a, before(b.getClass())).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void after_class() { + Filter a = new FilterA(); + Filter b = new FilterB(); + + Chain<Filter> chain = getChain(). + add(b, after(a.getClass())).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void before_subclass() { + Filter a = new FilterA(); + Filter b = new FilterExtendsB(); + + Chain<Filter> chain = getChain(). + add(b).add(a, before(FilterB.class)).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void after_subclass() { + Filter a = new FilterExtendsA(); + Filter b = new FilterB(); + + Chain<Filter> chain = getChain(). + add(b, after(FilterA.class)).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void before_provided_name() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b, provides("B")).add(a, before("B")).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void after_provided_name() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b, after("A")).add(a, provides("A")).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void before_provided_name_in_annotations() { + Filter providesA = new ProvidesA(); + Filter beforeA = new BeforeA(); + + Chain<Filter> chain = getChain(). + add(providesA).add(beforeA).build(); + + assertThat(chain, is(new Chain<>("myChain", beforeA, providesA))); + } + + @Test + public void after_provided_name_in_annotations() { + Filter providesA = new ProvidesA(); + Filter afterA = new AfterA(); + + Chain<Filter> chain = getChain(). + add(afterA).add(providesA).build(); + + assertThat(chain, is(new Chain<>("myChain", providesA, afterA))); + } + + @Test + public void before_all() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b).add(a, before("*")).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void after_all() { + Filter a = new Filter(); + Filter b = new Filter(); + + Chain<Filter> chain = getChain(). + add(b, after("*")).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void before_all_annotation() { + Filter a = new Filter(); + Filter beforeAll = new BeforeAll(); + + Chain<Filter> chain = getChain(). + add(a).add(beforeAll).build(); + + assertThat(chain, is(new Chain<>("myChain", beforeAll, a))); + } + + @Test + public void after_all_annotation() { + Filter a = new Filter(); + Filter afterAll = new AfterAll(); + + Chain<Filter> chain = getChain(). + add(afterAll).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, afterAll))); + } + + @Test + public void before_all_annotated_component_can_be_before_another_component_that_is_also_before_all_annotated() { + Filter beforeAll = new BeforeAll(); + Filter beforeBeforeAll = new BeforeBeforeAll(); + + Chain<Filter> chain = getChain(). + add(beforeAll).add(beforeBeforeAll).build(); + + assertThat(chain, is(new Chain<>("myChain", beforeBeforeAll, beforeAll))); + } + + @Test + public void after_all_annotated_component_can_be_after_another_component_that_is_also_after_all_annotated() { + Filter afterAll = new AfterAll(); + Filter afterAfterAll = new AfterAfterAll(); + + Chain<Filter> chain = getChain(). + add(afterAfterAll).add(afterAll).build(); + + assertThat(chain, is(new Chain<>("myChain", afterAll, afterAfterAll))); + } + + @Test + public void component_that_is_not_annotated_can_be_before_a_before_all_annotated_component() { + Filter first = new Filter(); + Filter beforeAll = new BeforeAll(); + + Chain<Filter> chain = getChain(). + add(beforeAll).add(first, before("BeforeAll")).build(); + + assertThat(chain, is(new Chain<>("myChain", first, beforeAll))); + } + + @Test + public void component_that_is_not_annotated_can_be_after_an_after_all_annotated_component() { + Filter last = new Filter(); + Filter afterAll = new AfterAll(); + + Chain<Filter> chain = getChain(). + add(last, after("AfterAll")).add(afterAll).build(); + + assertThat(chain, is(new Chain<>("myChain", afterAll, last))); + } + + @Test + public void class_name_is_always_provided() { + Filter a = new FilterA(); + Filter b = new FilterB(); + + Chain<Filter> chain = getChain(). + add(b, after(a.getClass().getName())).add(a).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b))); + } + + @Test + public void provides_annotation_on_superclass_is_inherited_by_subclasses() { + Filter extendsA = new ExtendsProvidesA(); + Filter first = new FilterA(); + Filter last = new FilterB(); + + Chain<Filter> chain = getChain(). + add(last, after("A")).add(first, before("A")).add(extendsA).build(); + + assertThat(chain, is(new Chain<>("myChain", first, extendsA, last))); + } + + @Test + public void provides_annotation_on_superclass_is_inherited_by_a_subclass_that_has_its_own_provides_annotation() { + Filter extendsA = new ProvidesA_and_ProvidesExtendsA(); + Filter first = new FilterA(); + Filter last = new FilterB(); + + Chain<Filter> chain = getChain(). + add(last, after("A")).add(first, before("ExtendsA")).add(extendsA).build(); + + assertThat(chain, is(new Chain<>("myChain", first, extendsA, last))); + } + + @Test + public void before_annotation_on_superclass_is_inherited_by_a_subclass_that_has_its_own_before_annotation() { + Filter beforeA_and_beforeB = new BeforeA_and_BeforeB(); + Filter A = new ProvidesA(); + Filter B = new ProvidesB(); + + Chain<Filter> chain = getChain(). + add(A, before("*")).add(beforeA_and_beforeB).add(B).build(); + assertThat(chain, is(new Chain<>("myChain", beforeA_and_beforeB, A, B))); + } + + @Test + public void add_accepts_multiple_dependencies() { + Filter a = new Filter(); + Filter b = new Filter(); + Filter c = new Filter(); + + Chain<Filter> chain = getChain(). + add(a).add(c).add(b, after(a), before(c)).build(); + + assertThat(chain, is(new Chain<>("myChain", a, b, c))); + } + + private ChainBuilder<Filter> getChain() { + return new ChainBuilder<>("myChain"); + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/chain/ChainTest.java b/yolean/src/test/java/com/yahoo/yolean/chain/ChainTest.java new file mode 100644 index 00000000000..e8a8f9309b0 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/chain/ChainTest.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +/** + * @author tonytv + */ +public class ChainTest { + + public static class Filter { + + } + + public static class OtherFilter extends Filter { + + } + + @Test + public void empty_chain_toString() { + Chain<Filter> c = new Chain<>("myChain"); + assertThat(c.toString(), is("chain 'myChain'{}")); + } + + @Test + public void singleton_chain_toString() { + Chain<Filter> c = new Chain<>("myChain", new Filter()); + assertThat(c.toString(), is("chain 'myChain'{ Filter }")); + } + + @Test + public void chain_toString() { + Chain<Filter> c = new Chain<>("myChain", new Filter(), new Filter(), new OtherFilter()); + assertThat(c.toString(), is("chain 'myChain'{ Filter -> Filter -> OtherFilter }")); + } + + @Test + public void non_equal_due_to_different_components() { + assertThat(new Chain<>("a", new Filter()), is(not(new Chain<>("a", new Filter())))); + } + + @Test + public void non_equal_due_to_different_size_comopnents() { + assertThat(new Chain<>("a", new Filter()), is(not(new Chain<Filter>("a")))); + } + + @Test + public void hashCode_equals() { + assertThat(new Chain<>("a").hashCode(), is(new Chain<Filter>("a").hashCode())); + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/chain/ContainsSameElements.java b/yolean/src/test/java/com/yahoo/yolean/chain/ContainsSameElements.java new file mode 100644 index 00000000000..9eee34a45a3 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/chain/ContainsSameElements.java @@ -0,0 +1,74 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.sort; + +/** + * @author tonytv + */ +class ContainsSameElements<T> extends TypeSafeMatcher<Collection<? super T>> { + + private final Set<T> identitySet; + + public static <T> Matcher<Collection<? super T>> containsSameElements(Collection<T> collection) { + return new ContainsSameElements<>(collection); + } + + public ContainsSameElements(Collection<T> collection) { + identitySet = toIdentitySet(collection); + } + + @SuppressWarnings("SuspiciousMethodCalls") + @Override + protected boolean matchesSafely(Collection<? super T> collection2) { + for (Object elem : collection2) { + if (!identitySet.contains(elem)) { + return false; + } + } + + return collection2.size() == identitySet.size(); + } + + @Override + public void describeTo(Description description) { + description.appendText("containsSameElements "); + appendCollection(description, identitySet); + } + + private void appendCollection(Description description, Collection<?> collection) { + description.appendValueList("{", ", ", "}", elementsToStringSorted(collection)); + } + + private List<String> elementsToStringSorted(Collection<?> collection) { + List<String> result = new ArrayList<>(); + for (Object o : collection) { + result.add(o.toString()); + } + sort(result); + return result; + } + + @Override + protected void describeMismatchSafely(Collection<? super T> collection, Description description) { + description.appendText("was "); + appendCollection(description, collection); + } + + public static <T> Set<T> toIdentitySet(Collection<? extends T> collection) { + Set<T> identitySet = Collections.newSetFromMap(new IdentityHashMap<T, Boolean>()); + identitySet.addAll(collection); + return identitySet; + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/chain/DirectedGraphTest.java b/yolean/src/test/java/com/yahoo/yolean/chain/DirectedGraphTest.java new file mode 100644 index 00000000000..5097253f8e1 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/chain/DirectedGraphTest.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.junit.Assert.assertThat; + +public class DirectedGraphTest { + + private DirectedGraph graph; + private Vertex[] v = new Vertex[10]; + + @Before + public void setup() { + for (int i = 0; i < v.length; i++) { + v[i] = new TestVertex(i); + } + + graph = new DirectedGraph(); + } + + @Test + public void before_all_are_prioritized_first() { + graph.addVertex(v[0]); + graph.addBeginningVertex(v[1]); + + assertThat(graph.topologicalSort(), contains(v[1], v[0])); + } + + @Test + public void vertex_can_be_placed_before_before_all_vertices() { + graph.addVertex(v[0]); + graph.addBeginningVertex(v[1]); + graph.addEdge(v[0], v[1]); + + assertThat(graph.topologicalSort(), contains(v[0], v[1])); + } + + static class TestVertex implements Vertex { + + private final int id; + + TestVertex(int id) { + this.id = id; + } + + @Override + public String toString() { + return "Vertex{" + id + '}'; + } + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/chain/EnumeratedIdentitySetTest.java b/yolean/src/test/java/com/yahoo/yolean/chain/EnumeratedIdentitySetTest.java new file mode 100644 index 00000000000..8a55381cdef --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/chain/EnumeratedIdentitySetTest.java @@ -0,0 +1,249 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.chain; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import static com.yahoo.yolean.chain.ContainsSameElements.containsSameElements; +import static com.yahoo.yolean.chain.ContainsSameElements.toIdentitySet; +import static java.util.Collections.singleton; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Tests for EnumeratedIdentitySet. + */ +public class EnumeratedIdentitySetTest { + + private final List<Element> elements; + + public EnumeratedIdentitySetTest() { + elements = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + elements.add(new Element()); + } + } + + @Test + public void size() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + assertThat(set.size(), is(elements.size())); + + set.add(elements.get(0)); + assertThat(set.size(), is(elements.size())); + + set.remove(elements.get(0)); + assertThat(set.size(), is(elements.size() - 1)); + } + + @Test + public void isEmpty() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(); + assertTrue(set.isEmpty()); + + set.add(elements.get(0)); + assertFalse(set.isEmpty()); + } + + @Test + public void contains2() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + assertTrue(set.contains(elements.get(0))); + assertFalse(set.contains(new Element())); + } + + @Test + public void iterator() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + + IdentityHashMap<Element, Void> collectedElements = new IdentityHashMap<>(); + int count = 0; + for (Element element : set) { + collectedElements.put(element, null); + count++; + } + + assertThat(collectedElements.size(), is(count)); + assertThat(collectedElements.size(), is(elements.size())); + + for (Element element : elements) { + assertTrue(collectedElements.containsKey(element)); + } + } + + @Test + public void toArray() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + + Object[] array = set.toArray(); + Element[] array2 = set.toArray(new Element[0]); + + assertThat(Arrays.asList(array), containsSameElements(set)); + assertThat(Arrays.asList(array2), containsSameElements(set)); + } + + @Test + public void add() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(); + assertTrue(set.add(elements.get(0))); + assertFalse(set.add(elements.get(0))); + } + + @Test + public void remove() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + assertTrue(set.remove(elements.get(0))); + assertFalse(set.remove(elements.get(0))); + } + + @Test + public void containsAll() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + assertTrue(set.containsAll(elements.subList(0, 7))); + assertTrue(set.containsAll(elements)); + + List<Element> extendedElements = new ArrayList<>(elements); + extendedElements.add(new Element()); + assertFalse(set.containsAll(extendedElements)); + } + + @Test + public void addAll() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(); + set.addAll(elements); + + assertThat(set, containsSameElements(elements)); + } + + @Test + public void retainAll() { + Set<Element> set = new EnumeratedIdentitySet<>(); + + set.addAll(elements.subList(0, 5)); + boolean changed = set.retainAll(toIdentitySet(elements.subList(3, 10))); + + assertTrue(changed); + assertThat(set, containsSameElements(elements.subList(3, 5))); + + changed = set.retainAll(toIdentitySet(elements)); + assertFalse(changed); + } + + @Test + public void removeAll() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + set.removeAll(elements.subList(0, 5)); + assertThat(set, containsSameElements(elements.subList(5, 10))); + } + + @Test + public void clear() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + set.clear(); + assertThat(set, empty()); + } + + @Test + public void removeNulls() { + Element[] singletonArray = { null, elements.get(0), null }; + assertThat(EnumeratedIdentitySet.removeNulls(singletonArray), + containsSameElements(Arrays.asList(elements.get(0)))); + + Element[] elementsWithNull = new Element[20]; + + Iterator<Element> iterator = elements.iterator(); + + copyElementsTo(iterator, elementsWithNull, 2, 1); + copyElementsTo(iterator, elementsWithNull, 4, 8); + copyElementsTo(iterator, elementsWithNull, 19, 1); + + assertThat(EnumeratedIdentitySet.removeNulls(elementsWithNull), containsSameElements(elements)); + } + + private void copyElementsTo(Iterator<Element> iterator, Element[] array, int startIndex, int numItems) { + for (int i = 0; i < numItems; i++) { + array[i + startIndex] = iterator.next(); + } + } + + @Test + public void renumber_preserves_ordering() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(); + + for (int i = 0; i < 200; i++) { + set.add(new Element()); + } + + set.addAll(elements); + + EnumeratedIdentitySet<Element> elementsToPreserve = new EnumeratedIdentitySet<>(elements); + + for (Iterator<Element> i = set.iterator(); i.hasNext(); ) { + if (!elementsToPreserve.contains(i.next())) { + i.remove(); + } + } + + List<Element> forceRenumber = set.insertionOrderedList(); + assertThat(forceRenumber.size(), is(elements.size())); + + for (int i = 0; i < elements.size(); i++) { + assertSame(forceRenumber.get(i), elements.get(i)); + } + + set.add(new Element()); + assertThat(set.numbers(), containsSameElements(range(0, 10))); + } + + @Test + public void renumber_when_empty() { + EnumeratedIdentitySet<Element> set = new EnumeratedIdentitySet<>(elements); + for (Iterator<Element> i = set.iterator(); i.hasNext(); ) { + i.next(); + i.remove(); + } + + set.insertionOrderedList(); + assertThat(set.numbers(), empty()); + + set.add(new Element()); + assertThat(set.numbers(), containsSameElements(singleton(0))); + } + + private List<Integer> range(int start, int endInclusive) { + List<Integer> result = new ArrayList<>(); + for (int i = start; i <= endInclusive; i++) { + result.add(i); + } + + return result; + } + + static class Element { + + @Override + public int hashCode() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return "Element@" + System.identityHashCode(this); + } + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java b/yolean/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java new file mode 100644 index 00000000000..280fa2bd22f --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/concurrent/CopyOnWriteHashMapTest.java @@ -0,0 +1,106 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import org.junit.Test; + +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a> + * @since 5.2 + */ +public class CopyOnWriteHashMapTest { + + @Test + public void requireThatAccessorsWork() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + assertEquals(0, map.size()); + assertEquals(true, map.isEmpty()); + assertEquals(false, map.containsKey("fooKey")); + assertEquals(false, map.containsValue("fooVal")); + assertNull(map.get("fooKey")); + assertNull(map.remove("fooKey")); + assertEquals(0, map.keySet().size()); + assertEquals(0, map.entrySet().size()); + assertEquals(0, map.values().size()); + + map.put("fooKey", "fooVal"); + assertEquals(1, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(true, map.containsKey("fooKey")); + assertEquals(true, map.containsValue("fooVal")); + assertEquals("fooVal", map.get("fooKey")); + assertEquals(1, map.keySet().size()); + assertEquals(1, map.entrySet().size()); + assertEquals(1, map.values().size()); + + map.put("barKey", "barVal"); + assertEquals(2, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(true, map.containsKey("fooKey")); + assertEquals(true, map.containsKey("barKey")); + assertEquals(true, map.containsValue("fooVal")); + assertEquals(true, map.containsValue("barVal")); + assertEquals("fooVal", map.get("fooKey")); + assertEquals("barVal", map.get("barKey")); + assertEquals(2, map.keySet().size()); + assertEquals(2, map.entrySet().size()); + assertEquals(2, map.values().size()); + + assertEquals("fooVal", map.remove("fooKey")); + assertEquals(1, map.size()); + assertEquals(false, map.isEmpty()); + assertEquals(false, map.containsKey("fooKey")); + assertEquals(true, map.containsKey("barKey")); + assertEquals(false, map.containsValue("fooVal")); + assertEquals(true, map.containsValue("barVal")); + assertNull(map.get("fooKey")); + assertEquals("barVal", map.get("barKey")); + assertEquals(1, map.keySet().size()); + assertEquals(1, map.entrySet().size()); + assertEquals(1, map.values().size()); + } + + @Test + public void requireThatEntrySetDoesNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + Map.Entry<String, String> entry = it.next(); + assertEquals("fooKey", entry.getKey()); + assertEquals("fooVal", entry.getValue()); + } + + @Test + public void requireThatKeySetDoesNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<String> it = map.keySet().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + assertEquals("fooKey", it.next()); + } + + @Test + public void requireThatValuesDoNotReflectConcurrentModifications() { + Map<String, String> map = new CopyOnWriteHashMap<>(); + map.put("fooKey", "fooVal"); + + Iterator<String> it = map.values().iterator(); + assertEquals("fooVal", map.remove("fooKey")); + + assertTrue(it.hasNext()); + assertEquals("fooVal", it.next()); + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java b/yolean/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java new file mode 100644 index 00000000000..794a8f4e75f --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/concurrent/ThreadRobustListTestCase.java @@ -0,0 +1,146 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.concurrent; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ThreadRobustListTestCase { + + private final static int NUM_THREADS = 64; + private final static int NUM_ITEMS_TO_WRITE = 1000000; + private final static int NUM_TIMES_TO_READ = 10; + + @Test + public void requireThatListIsThreadRobust() throws Exception { + final CountDownLatch latch = new CountDownLatch(NUM_THREADS); + final ThreadRobustList<Integer> sharedList = new ThreadRobustList<>(); + + List<Callable<Boolean>> tasks = new ArrayList<>(NUM_THREADS); + tasks.add(new WriterTask(latch, sharedList)); + for (int i = 1; i < NUM_THREADS; ++i) { + tasks.add(new ReaderTask(latch, sharedList)); + } + for (Future<Boolean> result : Executors.newFixedThreadPool(NUM_THREADS).invokeAll(tasks)) { + assertTrue(result.get(60, TimeUnit.SECONDS)); + } + } + + @Test + public void requireThatAccessorsWork() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + assertTrue(lst.isEmpty()); + assertFalse(lst.iterator().hasNext()); + + Object foo = new Object(); + lst.add(foo); + assertFalse(lst.isEmpty()); + Iterator<Object> it = lst.iterator(); + assertNotNull(it); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertFalse(it.hasNext()); + + Object bar = new Object(); + lst.add(bar); + assertFalse(lst.isEmpty()); + assertNotNull(it = lst.iterator()); + assertTrue(it.hasNext()); + assertSame(foo, it.next()); + assertTrue(it.hasNext()); + assertSame(bar, it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatIteratorNextThrowsNoSuchElementExceptionWhenDone() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + Iterator<Object> it = lst.iterator(); + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } catch (NoSuchElementException e) { + + } + } + + @Test + public void requireThatIteratorRemoveIsNotSupported() { + ThreadRobustList<Object> lst = new ThreadRobustList<>(); + Object obj = new Object(); + lst.add(obj); + Iterator<Object> it = lst.iterator(); + assertTrue(it.hasNext()); + assertSame(obj, it.next()); + try { + it.remove(); + fail(); + } catch (UnsupportedOperationException e) { + + } + } + + private static class WriterTask implements Callable<Boolean> { + + final CountDownLatch latch; + final ThreadRobustList<Integer> sharedList; + + WriterTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) { + this.latch = latch; + this.sharedList = sharedList; + } + + @Override + public Boolean call() throws Exception { + latch.countDown(); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + for (int i = 0; i < NUM_ITEMS_TO_WRITE; ++i) { + sharedList.add(i); + } + return true; + } + } + + private static class ReaderTask implements Callable<Boolean> { + + final CountDownLatch latch; + final ThreadRobustList<Integer> sharedList; + + ReaderTask(CountDownLatch latch, ThreadRobustList<Integer> sharedList) { + this.latch = latch; + this.sharedList = sharedList; + } + + @Override + public Boolean call() throws Exception { + latch.countDown(); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + for (int i = 0; i < NUM_TIMES_TO_READ; ++i) { + Iterator<Integer> it = sharedList.iterator(); + for (int j = 0; it.hasNext(); ++j) { + assertEquals(j, it.next().intValue()); + } + } + return true; + } + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java b/yolean/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java new file mode 100644 index 00000000000..cc25ecf4cd0 --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/trace/TraceNodeTestCase.java @@ -0,0 +1,231 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class TraceNodeTestCase { + + @Test + public void requireThatAccessorsWork() { + TraceNode node = new TraceNode(null, 6); + assertNull(node.payload()); + assertEquals(6, node.timestamp()); + assertFalse(node.children().iterator().hasNext()); + assertFalse(node.descendants(Object.class).iterator().hasNext()); + assertTrue(node.isRoot()); + assertNull(node.parent()); + assertSame(node, node.root()); + } + + @Test + public void requireThatToStringIsReadable() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 1)) + .add(new TraceNode("b", 2) + .add(new TraceNode("c", 3))); + assertEquals("[ [ a b [ c ] ] ]", trace.toString()); + } + + @Test + public void requireThatPayloadMayBeNull() { + TraceNode node = new TraceNode(null, 6); + assertNull(node.payload()); + } + + @Test + public void requireThatRootNodesCanBeAdded() { + TraceNode parent = new TraceNode(null, 1); + + TraceNode foo = new TraceNode(null, 2); + parent.add(foo); + assertSame(parent, foo.parent()); + + TraceNode bar = new TraceNode(null, 3); + parent.add(bar); + assertSame(parent, bar.parent()); + + Iterator<TraceNode> children = parent.children().iterator(); + assertTrue(children.hasNext()); + assertSame(foo, children.next()); + assertTrue(children.hasNext()); + assertSame(bar, children.next()); + assertFalse(children.hasNext()); + + Iterator<Object> payloads = parent.descendants(Object.class).iterator(); + assertFalse(payloads.hasNext()); + } + + @Test + public void requireThatNonRootNodeCanNotBeAdded() { + TraceNode foo = new TraceNode(null, 0); + TraceNode bar = new TraceNode(null, 0); + TraceNode baz = new TraceNode(null, 0); + bar.add(baz); + try { + foo.add(baz); + fail(); + } catch (IllegalArgumentException e) { + + } + assertSame(bar, baz.parent()); + assertTrue(bar.children().iterator().hasNext()); + assertFalse(foo.children().iterator().hasNext()); + } + + @Test + public void requireThatChildrenIsNeverNull() { + assertNotNull(new TraceNode(null, 69).children()); + } + + @Test + public void requireThatDescendantsIsNeverNull() { + assertNotNull(new TraceNode(null, 69).descendants(Object.class)); + } + + @Test + public void requireThatDescendantsOrderIsDepthFirstPrefix() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode("e", 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + Iterator<String> it = trace.descendants(String.class).iterator(); + assertTrue(it.hasNext()); + assertEquals("a", it.next()); + assertTrue(it.hasNext()); + assertEquals("b", it.next()); + assertTrue(it.hasNext()); + assertEquals("c", it.next()); + assertTrue(it.hasNext()); + assertEquals("d", it.next()); + assertTrue(it.hasNext()); + assertEquals("e", it.next()); + assertTrue(it.hasNext()); + assertEquals("f", it.next()); + assertTrue(it.hasNext()); + assertEquals("g", it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatDescendantsFilterPayloads() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode(69, 0)) + .add(new TraceNode("b", 0) + .add(new TraceNode("c", 0)) + .add(new TraceNode(new Object(), 0)))) + .add(new TraceNode("d", 0) + .add(new TraceNode("e", 0))); + + Iterator<String> it = trace.descendants(String.class).iterator(); + assertTrue(it.hasNext()); + assertEquals("a", it.next()); + assertTrue(it.hasNext()); + assertEquals("b", it.next()); + assertTrue(it.hasNext()); + assertEquals("c", it.next()); + assertTrue(it.hasNext()); + assertEquals("d", it.next()); + assertTrue(it.hasNext()); + assertEquals("e", it.next()); + assertFalse(it.hasNext()); + } + + @Test + public void requireThatVisitorOrderIsDepthFirstPrefix() { + TraceNode trace = new TraceNode(null, 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode(3, 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + final List<Object> payloads = new ArrayList<>(); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + payloads.add(node.payload()); + } + }); + assertEquals(Arrays.<Object>asList(null, "a", "b", "c", "d", 3, "f", "g"), + payloads); + } + + @Test + public void requireThatVisitorDoesNotEnterOrLeaveNodesThatHaveNoChildren() { + TraceNode trace = new TraceNode(null, 0); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + + } + + @Override + public void entering(TraceNode node) { + fail(); + } + + @Override + public void leaving(TraceNode node) { + fail(); + } + }); + } + + @Test + public void requireThatVisitorEntersAndLeavesNodesThatHaveChildren() { + TraceNode trace = new TraceNode("", 0) + .add(new TraceNode("a", 0) + .add(new TraceNode("b", 0)) + .add(new TraceNode("c", 0) + .add(new TraceNode("d", 0)) + .add(new TraceNode("e", 0)))) + .add(new TraceNode("f", 0) + .add(new TraceNode("g", 0))); + + final StringBuilder out = new StringBuilder(); + trace.accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + out.append(node.payload()); + } + + @Override + public void entering(TraceNode node) { + out.append("["); + } + + @Override + public void leaving(TraceNode node) { + out.append("]"); + } + }); + assertEquals("[a[bc[de]]f[g]]", out.toString()); + } +} diff --git a/yolean/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java b/yolean/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java new file mode 100644 index 00000000000..3b04c210c2f --- /dev/null +++ b/yolean/src/test/java/com/yahoo/yolean/trace/TraceVisitorTestCase.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.yolean.trace; + +import org.junit.Test; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class TraceVisitorTestCase { + + @Test + public void requireThatTraceVisitorCompilesWithOnlyVisitImplemented() { + new TraceNode(null, 0).accept(new TraceVisitor() { + + @Override + public void visit(TraceNode node) { + + } + }); + } +} |