summaryrefslogtreecommitdiffstats
path: root/container-di/src/test
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-di/src/test
Publish
Diffstat (limited to 'container-di/src/test')
-rw-r--r--container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java75
-rw-r--r--container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java93
-rw-r--r--container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java39
-rw-r--r--container-di/src/test/java/demo/Base.java66
-rw-r--r--container-di/src/test/java/demo/ComponentConfigTest.java48
-rw-r--r--container-di/src/test/java/demo/ComponentRegistryTest.java42
-rw-r--r--container-di/src/test/java/demo/ContainerTestBase.java74
-rw-r--r--container-di/src/test/java/demo/DeconstructTest.java35
-rw-r--r--container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java104
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala88
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala350
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala49
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala538
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala66
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala249
-rw-r--r--container-di/src/test/vespa-configdef/bootstrap1.def5
-rw-r--r--container-di/src/test/vespa-configdef/bootstrap2.def6
-rw-r--r--container-di/src/test/vespa-configdef/components1.def5
-rw-r--r--container-di/src/test/vespa-configdef/int.def6
-rw-r--r--container-di/src/test/vespa-configdef/string.def6
-rw-r--r--container-di/src/test/vespa-configdef/test.def6
-rw-r--r--container-di/src/test/vespa-configdef/test2.def6
-rw-r--r--container-di/src/test/vespa-configdef/thread-pool.def6
23 files changed, 1962 insertions, 0 deletions
diff --git a/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java b/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java
new file mode 100644
index 00000000000..4b0c8fca4d1
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+public class ComponentSpecTestCase extends TestCase {
+
+ public void testMatches() {
+ ComponentId a = new ComponentId("test:1");
+ ComponentId b = new ComponentId("test:1.1.1");
+ ComponentId c = new ComponentId("test:2");
+ ComponentId d = new ComponentId("test:3");
+ ComponentId e = new ComponentId("test");
+
+ ComponentSpecification aspec = new ComponentSpecification("test");
+ ComponentSpecification bspec = new ComponentSpecification("test:1");
+ ComponentSpecification cspec = new ComponentSpecification("test:2");
+ ComponentSpecification dspec = new ComponentSpecification("test1:2");
+
+ assertTrue(aspec.matches(a));
+ assertTrue(aspec.matches(b));
+ assertTrue(aspec.matches(c));
+ assertTrue(aspec.matches(d));
+ assertTrue(aspec.matches(e));
+
+ assertTrue(bspec.matches(a));
+ assertTrue(bspec.matches(b));
+ assertFalse(bspec.matches(c));
+ assertFalse(bspec.matches(d));
+ assertFalse(bspec.matches(e));
+
+ assertFalse(cspec.matches(a));
+ assertFalse(cspec.matches(b));
+ assertTrue(cspec.matches(c));
+ assertFalse(cspec.matches(d));
+ assertFalse(cspec.matches(e));
+
+ assertFalse(dspec.matches(a));
+ assertFalse(dspec.matches(b));
+ assertFalse(dspec.matches(c));
+ assertFalse(dspec.matches(d));
+ assertFalse(dspec.matches(e));
+
+ }
+
+ public void testMatchesWithNamespace() {
+ ComponentId namespace = new ComponentId("namespace:2");
+
+ ComponentId a = new ComponentId("test", new Version(1), namespace);
+ ComponentId b = new ComponentId("test:1@namespace:2");
+ ComponentId c = new ComponentId("test:1@namespace");
+ assertEquals(a, b);
+ assertFalse(a.equals(c));
+
+ ComponentSpecification spec = new ComponentSpecification("test", null, namespace);
+ assertTrue(spec.matches(a));
+ assertTrue(spec.matches(b));
+ assertFalse(spec.matches(c));
+ }
+
+ public void testStringValue() {
+ assertStringValueEqualsInputSpec("a:1.0.0.alpha@namespace");
+ assertStringValueEqualsInputSpec("a:1.0.0.alpha");
+ assertStringValueEqualsInputSpec("a:1.0");
+ assertStringValueEqualsInputSpec("a");
+ }
+
+ private void assertStringValueEqualsInputSpec(String componentSpec) {
+ assertEquals(componentSpec,
+ new ComponentSpecification(componentSpec).stringValue());
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java b/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java
new file mode 100644
index 00000000000..b14d56dd31d
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+
+/**
+ * Tests that ComponentRegistry handles namespaces correctly.
+ * @author tonytv
+ */
+public class ComponentRegistryTestCase {
+ private static class TestComponent extends AbstractComponent {
+ TestComponent(ComponentId componentId) {
+ super(componentId);
+ }
+ }
+
+ private static final String componentName = "component";
+
+ private static final String namespace1 = "namespace1";
+ private static final String namespace2 = "namespace2";
+ private static final String namespace21 = "namespace2:1";
+
+ private static final TestComponent component1 = componentInNamespace(namespace1);
+ private static final TestComponent component2 = componentInNamespace(namespace2);
+ private static final TestComponent component21 = componentInNamespace(namespace21);
+
+ private ComponentRegistry<TestComponent> registry;
+
+ private static ComponentSpecification specInNamespace(String namespace) {
+ return new ComponentSpecification(componentName + "@" + namespace);
+ }
+
+ private static ComponentId idInNamespace(String namespace) {
+ return specInNamespace(namespace).toId();
+ }
+
+ private static TestComponent componentInNamespace(String namespace) {
+ return new TestComponent(idInNamespace(namespace));
+ }
+
+ @Before
+ public void before() {
+ registry = new ComponentRegistry<>();
+
+ registry.register(component1.getId(), component1);
+ registry.register(component2.getId(), component2);
+ registry.register(component21.getId(), component21);
+ }
+
+ @Test
+ public void testAllPresent() {
+ assertEquals(3, registry.getComponentCount());
+ }
+
+ @Test
+ public void testIdNamespaceLookup() {
+ assertEquals(component1, registry.getComponent(idInNamespace(namespace1)));
+ assertEquals(component2, registry.getComponent(idInNamespace(namespace2)));
+ assertEquals(component21, registry.getComponent(idInNamespace(namespace21)));
+ }
+
+ @Test
+ public void testSpecNamespaceLookup() {
+ assertEquals(component1, registry.getComponent(specInNamespace(namespace1)));
+
+ // Version for namespace must match the specification exactly, so do not return version '1' when an
+ // empty version is asked for.
+ assertEquals(component2, registry.getComponent(specInNamespace(namespace2)));
+ assertEquals(component21, registry.getComponent(specInNamespace(namespace21)));
+ }
+
+ @Test
+ public void testInnerComponentNotMixedWithTopLevelComponent() {
+ assertNull(registry.getComponent(componentName));
+
+ TestComponent topLevel = new TestComponent(new ComponentId(componentName));
+ registry.register(topLevel.getId(), topLevel);
+ assertEquals(topLevel, registry.getComponent(componentName));
+
+ assertEquals(component1, registry.getComponent(specInNamespace(namespace1)));
+ assertEquals(component1, registry.getComponent(idInNamespace(namespace1)));
+ }
+
+}
diff --git a/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java b/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java
new file mode 100644
index 00000000000..7359cc8ba66
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.test;
+
+import com.yahoo.component.ComponentId;
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ComponentIdTestCase extends TestCase {
+
+ public void testFileNameConversion() {
+ assertFileNameEquals("a","a");
+ assertFileNameEquals("a-1","a-1");
+ assertFileNameEquals("a-1","a-1.0.0");
+ assertFileNameEquals("a-1.0.0.qualifier","a-1.0.0.qualifier");
+ assertFileNameEquals("a.b-1.0.0.qualifier","a.b-1.0.0.qualifier");
+ assertFileNameEquals("a@space","a@space");
+ assertFileNameEquals("a-1@space","a-1@space");
+ assertFileNameEquals("a-1@space","a-1.0.0@space");
+ assertFileNameEquals("a-1.0.0.qualifier@space","a-1.0.0.qualifier@space");
+ assertFileNameEquals("a.b-1.0.0.qualifier@space","a.b-1.0.0.qualifier@space");
+ }
+
+ /** Takes two id file names as input */
+ private void assertFileNameEquals(String expected,String initial) {
+ assertEquals("'" + initial + "' became id '" + ComponentId.fromFileName(initial) + "' which should become '" + expected + "'",
+ expected,ComponentId.fromFileName(initial).toFileName());
+ }
+
+ public void testCompareWithNameSpace() {
+ ComponentId withNS = ComponentId.fromString("foo@ns");
+ ComponentId withoutNS = ComponentId.fromString("foo"); // Should be less than withNs
+
+ assertEquals(withNS.compareTo(withoutNS), 1);
+ assertEquals(withoutNS.compareTo(withNS), -1);
+ }
+
+}
diff --git a/container-di/src/test/java/demo/Base.java b/container-di/src/test/java/demo/Base.java
new file mode 100644
index 00000000000..7bc2c7b4404
--- /dev/null
+++ b/container-di/src/test/java/demo/Base.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.container.di.ContainerTest;
+import com.yahoo.container.di.componentgraph.core.ComponentGraph;
+import com.yahoo.container.di.componentgraph.core.ComponentNode;
+import com.yahoo.container.di.componentgraph.core.Node;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Before;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class Base {
+ private ComponentGraph componentGraph;
+ private Injector injector;
+ private Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs =
+ new HashMap<>();
+
+ @Before
+ public void createGraph() {
+ injector = Guice.createInjector();
+ componentGraph = new ComponentGraph(0);
+ }
+
+ public void register(Class<?> componentClass) {
+ componentGraph.add(mockComponentNode(componentClass));
+ }
+
+ public ComponentId toId(Class<?> componentClass) {
+ return ComponentId.fromString(componentClass.getName());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Node mockComponentNode(Class<?> componentClass) {
+ return new ComponentNode(toId(componentClass), toId(componentClass).toString(), (Class<Object>)componentClass, null);
+ }
+
+ public <T> T getInstance(Class<T> componentClass) {
+ return componentGraph.getInstance(componentClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void complete() {
+ componentGraph.complete(injector);
+ componentGraph.setAvailableConfigs(ContainerTest.convertMap(configs));
+ }
+
+ public void setInjector(Injector injector) {
+ this.injector = injector;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void addConfig(ConfigInstance configInstance, ComponentId id) {
+ configs.put(new ConfigKey<>((Class<ConfigInstance>)configInstance.getClass(), id.toString()),
+ configInstance);
+ }
+}
diff --git a/container-di/src/test/java/demo/ComponentConfigTest.java b/container-di/src/test/java/demo/ComponentConfigTest.java
new file mode 100644
index 00000000000..5b3be2f549a
--- /dev/null
+++ b/container-di/src/test/java/demo/ComponentConfigTest.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.config.test.ThreadPoolConfig;
+import com.yahoo.container.di.componentgraph.Provider;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ComponentConfigTest extends Base {
+ public static class ThreadPoolExecutorProvider implements Provider<Executor> {
+ private ExecutorService executor;
+
+ public ThreadPoolExecutorProvider(ThreadPoolConfig config) {
+ executor = Executors.newFixedThreadPool(config.numThreads());
+ }
+
+ @Override
+ public Executor get() {
+ return executor;
+ }
+
+ @Override
+ public void deconstruct() {
+ executor.shutdown();
+ }
+ }
+
+ @Test
+ public void require_that_non_components_can_be_configured() {
+ register(ThreadPoolExecutorProvider.class);
+ addConfig(new ThreadPoolConfig(new ThreadPoolConfig.Builder().numThreads(4)),
+ toId(ThreadPoolExecutorProvider.class));
+ complete();
+
+ Executor executor = getInstance(Executor.class);
+ assertNotNull(executor);
+ }
+}
diff --git a/container-di/src/test/java/demo/ComponentRegistryTest.java b/container-di/src/test/java/demo/ComponentRegistryTest.java
new file mode 100644
index 00000000000..193f65048ab
--- /dev/null
+++ b/container-di/src/test/java/demo/ComponentRegistryTest.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.provider.ComponentRegistry;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ComponentRegistryTest extends Base {
+ public static class SearchHandler extends AbstractComponent {
+ private final ComponentRegistry<Searcher> searchers;
+
+ public SearchHandler(ComponentRegistry<Searcher> searchers) {
+ this.searchers = searchers;
+ }
+ }
+
+ public static class Searcher extends AbstractComponent {}
+
+ public static class FooSearcher extends Searcher {}
+ public static class BarSearcher extends Searcher {}
+
+ @Test
+ public void require_that_component_registry_can_be_injected() {
+ register(SearchHandler.class);
+ register(FooSearcher.class);
+ register(BarSearcher.class);
+ complete();
+
+ SearchHandler handler = getInstance(SearchHandler.class);
+
+ ComponentRegistry<Searcher> searchers = handler.searchers;
+ assertNotNull(searchers.getComponent(toId(FooSearcher.class)));
+ assertNotNull(searchers.getComponent(toId(BarSearcher.class)));
+ }
+}
diff --git a/container-di/src/test/java/demo/ContainerTestBase.java b/container-di/src/test/java/demo/ContainerTestBase.java
new file mode 100644
index 00000000000..fdc73913052
--- /dev/null
+++ b/container-di/src/test/java/demo/ContainerTestBase.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 demo;
+
+import com.google.inject.Guice;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.config.FileReference;
+import com.yahoo.container.di.CloudSubscriberFactory;
+import com.yahoo.container.di.Container;
+import com.yahoo.container.di.ContainerTest;
+import com.yahoo.container.di.Osgi;
+import com.yahoo.container.di.componentgraph.core.ComponentGraph;
+import org.junit.Before;
+import org.osgi.framework.Bundle;
+import scala.collection.*;
+import scala.collection.immutable.*;
+import scala.collection.immutable.Set;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ContainerTestBase extends ContainerTest {
+ private ComponentGraph componentGraph;
+
+ @Before
+ public void createGraph() {
+ componentGraph = new ComponentGraph(0);
+ }
+
+ public void complete() {
+ try {
+ Container container = new Container(
+ new CloudSubscriberFactory(dirConfigSource().configSource()),
+ dirConfigSource().configId(),
+ new ContainerTest.TestDeconstructor(),
+ new Osgi() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public Class<Object> resolveClass(BundleInstantiationSpecification spec) {
+ try {
+ return (Class<Object>) Class.forName(spec.classId.getName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public BundleClasses getBundleClasses(ComponentSpecification bundle,
+ Set<String> packagesToScan) {
+ throw new UnsupportedOperationException("getBundleClasses not supported");
+ }
+
+ @Override
+ public void useBundles(Collection<FileReference> bundles) {}
+
+ @Override
+ public Bundle getBundle(ComponentSpecification spec) {
+ throw new UnsupportedOperationException("getBundle not supported.");
+ }
+ });
+ componentGraph = container.runOnce(componentGraph, Guice.createInjector());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public <T> T getInstance(Class<T> componentClass) {
+ return componentGraph.getInstance(componentClass);
+ }
+}
diff --git a/container-di/src/test/java/demo/DeconstructTest.java b/container-di/src/test/java/demo/DeconstructTest.java
new file mode 100644
index 00000000000..10e9844f6de
--- /dev/null
+++ b/container-di/src/test/java/demo/DeconstructTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.container.di.ContainerTest;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class DeconstructTest extends ContainerTestBase {
+ public static class DeconstructableComponent extends ContainerTest.DestructableComponent {
+ private boolean isDeconstructed = false;
+
+ @Override
+ public void deconstruct() {
+ isDeconstructed = true;
+ }
+ }
+
+ @Test
+ public void require_that_unused_components_are_deconstructed() {
+ writeBootstrapConfigs("d1", DeconstructableComponent.class);
+ complete();
+
+ DeconstructableComponent d1 = getInstance(DeconstructableComponent.class);
+
+ writeBootstrapConfigs("d2", DeconstructableComponent.class);
+ complete();
+
+ assertTrue(d1.isDeconstructed);
+ }
+}
diff --git a/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java b/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java
new file mode 100644
index 00000000000..3d5550ab0a3
--- /dev/null
+++ b/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.yahoo.component.AbstractComponent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+@SuppressWarnings("unused")
+public class FallbackToGuiceInjectorTest extends Base {
+ public static class MyComponent extends AbstractComponent {
+ private final String url;
+ private final Executor executor;
+
+ @Inject
+ public MyComponent(@Named("url") String url, Executor executor) {
+ this.url = url;
+ this.executor = executor;
+ }
+
+ public MyComponent() {
+ throw new RuntimeException("Constructor annotated with @Inject is preferred.");
+ }
+ }
+
+ public static class ComponentTakingDefaultString{
+ private final String injectedString;
+
+ public ComponentTakingDefaultString(String empty_string_created_by_guice) {
+ this.injectedString = empty_string_created_by_guice;
+ }
+ }
+
+ public static class ComponentThatCannotBeConstructed {
+ public ComponentThatCannotBeConstructed(Integer cannot_be_injected_because_Integer_has_no_default_ctor) { }
+ }
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void guice_injector_is_used_when_no_global_component_exists() {
+ setInjector(
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(Executor.class).toInstance(Executors.newSingleThreadExecutor());
+ bind(String.class).annotatedWith(Names.named("url")).toInstance("http://yahoo.com");
+ }
+ }));
+
+ register(MyComponent.class);
+ complete();
+
+ MyComponent component = getInstance(MyComponent.class);
+ assertThat(component.url, is("http://yahoo.com"));
+ assertNotNull(component.executor);
+ }
+
+ @Test
+ public void guice_injector_creates_a_new_instance_with_default_ctor_when_no_explicit_binding_exists() {
+ setInjector(emptyGuiceInjector());
+ register(ComponentTakingDefaultString.class);
+ complete();
+
+ ComponentTakingDefaultString component = getInstance(ComponentTakingDefaultString.class);
+ assertThat(component.injectedString, is(""));
+ }
+
+ @Test
+ public void guice_injector_fails_when_no_explicit_binding_exists_and_class_has_no_default_ctor() {
+ setInjector(emptyGuiceInjector());
+ register(ComponentThatCannotBeConstructed.class);
+
+ exception.expect(RuntimeException.class);
+ exception.expectMessage("When resolving dependencies of 'demo.FallbackToGuiceInjectorTest$ComponentThatCannotBeConstructed'");
+ complete();
+ }
+
+ private Injector emptyGuiceInjector() {
+ return Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ }
+ });
+ }
+}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala b/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala
new file mode 100644
index 00000000000..3eff01eed55
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.{is, instanceOf => hamcrestInstanceOf}
+import com.yahoo.vespa.config.ConfigKey
+import com.yahoo.config.test.{TestConfig, Bootstrap2Config, Bootstrap1Config}
+import com.yahoo.container.di.ConfigRetriever.{ComponentsConfigs, BootstrapConfigs}
+import org.junit.{Ignore, After, Before, Test}
+import scala.collection.JavaConversions._
+import scala.reflect.ClassTag
+import org.hamcrest.Matcher
+
+/**
+ *
+ * @author gjoranv
+ * @author tonytv
+ */
+class ConfigRetrieverTest {
+ var dirConfigSource: DirConfigSource = null
+
+ @Before def setup() {
+ dirConfigSource = new DirConfigSource("ConfigRetrieverTest-")
+ }
+ @After def cleanup() { dirConfigSource.cleanup() }
+
+ @Test
+ def require_that_bootstrap_configs_come_first() {
+ writeConfigs()
+ val retriever = createConfigRetriever()
+ val bootstrapConfigs = retriever.getConfigs(Set(), 0)
+
+ assertThat(bootstrapConfigs, instanceOf[BootstrapConfigs])
+ }
+
+ @Test
+ def require_that_components_comes_after_bootstrap() {
+ writeConfigs()
+ val retriever = createConfigRetriever()
+ val bootstrapConfigs = retriever.getConfigs(Set(), 0)
+
+ val testConfigKey = new ConfigKey(classOf[TestConfig], dirConfigSource.configId)
+ val componentsConfigs = retriever.getConfigs(Set(testConfigKey), 0)
+
+ componentsConfigs match {
+ case ComponentsConfigs(configs) => assertThat(configs.size, is(3))
+ case _ => fail("ComponentsConfigs has unexpected type: " + componentsConfigs)
+ }
+ }
+
+ @Test(expected = classOf[IllegalArgumentException])
+ @Ignore
+ def require_exception_upon_modified_components_keys_without_bootstrap() {
+ writeConfigs()
+ val retriever = createConfigRetriever()
+ val testConfigKey = new ConfigKey(classOf[TestConfig], dirConfigSource.configId)
+ val bootstrapConfigs = retriever.getConfigs(Set(), 0)
+ val componentsConfigs = retriever.getConfigs(Set(testConfigKey), 0)
+ retriever.getConfigs(Set(testConfigKey, new ConfigKey(classOf[TestConfig],"")), 0)
+ }
+
+ @Test
+ def require_that_empty_components_keys_after_bootstrap_returns_components_configs() {
+ writeConfigs()
+ val retriever = createConfigRetriever()
+ assertThat(retriever.getConfigs(Set(), 0), instanceOf[BootstrapConfigs])
+ assertThat(retriever.getConfigs(Set(), 0), instanceOf[ComponentsConfigs])
+ }
+
+ def writeConfigs() {
+ writeConfig("bootstrap1", """dummy "ignored" """")
+ writeConfig("bootstrap2", """dummy "ignored" """")
+ writeConfig("test", """stringVal "ignored" """")
+ }
+
+ def createConfigRetriever() = {
+ val configId = dirConfigSource.configId
+ val subscriber = new CloudSubscriberFactory(dirConfigSource.configSource)
+ new ConfigRetriever(
+ Set(new ConfigKey(classOf[Bootstrap1Config], configId),
+ new ConfigKey(classOf[Bootstrap2Config], configId)),
+ subscriber.getSubscriber(_))
+ }
+
+ def writeConfig = dirConfigSource.writeConfig _
+
+ def instanceOf[T: ClassTag] = hamcrestInstanceOf(implicitly[ClassTag[T]].runtimeClass): Matcher[AnyRef]
+}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala b/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala
new file mode 100644
index 00000000000..b64de80e39f
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala
@@ -0,0 +1,350 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{SimpleComponent2, SimpleComponent}
+import com.yahoo.container.di.componentgraph.Provider
+import com.yahoo.container.di.componentgraph.core.{Node, ComponentGraph}
+import org.junit.{Test, Before, After}
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers._
+import com.yahoo.config.test.TestConfig
+import com.yahoo.component.AbstractComponent
+import ContainerTest._
+import scala.collection.JavaConversions
+import com.yahoo.config.di.IntConfig
+import scala.concurrent.{future, Await}
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.util.Try
+import com.yahoo.container.di.config.RestApiContext
+import com.yahoo.container.bundle.MockBundle
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+class ContainerTest {
+ var dirConfigSource: DirConfigSource = null
+
+ @Before def setup() {
+ dirConfigSource = new DirConfigSource("ContainerTest-")
+ }
+
+ @After def cleanup() {
+ dirConfigSource.cleanup()
+ }
+
+ @Test
+ def components_can_be_created_from_config() {
+ writeBootstrapConfigs()
+ dirConfigSource.writeConfig("test", """stringVal "myString" """)
+
+ val container = newContainer(dirConfigSource)
+
+ val component = createComponentTakingConfig(container.runOnce())
+ assertThat(component.config.stringVal(), is("myString"))
+
+ container.shutdownConfigurer()
+ }
+
+ @Test
+ def components_are_reconfigured_after_config_update_without_bootstrap_configs() {
+ writeBootstrapConfigs()
+ dirConfigSource.writeConfig("test", """stringVal "original" """)
+
+ val container = newContainer(dirConfigSource)
+
+ val componentGraph = container.runOnce()
+ val component = createComponentTakingConfig(componentGraph)
+
+ assertThat(component.config.stringVal(), is("original"))
+
+ // Reconfigure
+ dirConfigSource.writeConfig("test", """stringVal "reconfigured" """)
+ container.reloadConfig(2)
+
+ val newComponentGraph = container.runOnce(componentGraph)
+ val component2 = createComponentTakingConfig(newComponentGraph)
+ assertThat(component2.config.stringVal(), is("reconfigured"))
+
+ container.shutdownConfigurer()
+ }
+
+ @Test
+ def graph_is_updated_after_bootstrap_update() {
+ dirConfigSource.writeConfig("test", """stringVal "original" """)
+ writeBootstrapConfigs("id1")
+
+ val container = newContainer(dirConfigSource)
+
+ val graph = container.runOnce()
+ val component = createComponentTakingConfig(graph)
+ assertThat(component.getId.toString, is("id1"))
+
+ writeBootstrapConfigsWithMultipleComponents(Array(
+ ("id1", classOf[ComponentTakingConfig]),
+ ("id2", classOf[ComponentTakingConfig])))
+
+ container.reloadConfig(2)
+ val newGraph = container.runOnce(graph)
+
+ assertThat(ComponentGraph.getNode(newGraph, "id1"), notNullValue(classOf[Node]));
+ assertThat(ComponentGraph.getNode(newGraph, "id2"), notNullValue(classOf[Node]));
+
+ container.shutdownConfigurer()
+ }
+
+ //@Test TODO
+ def deconstructor_is_given_guice_components() {
+ }
+
+ @Test
+ def osgi_component_is_deconstructed_when_not_reused() {
+ writeBootstrapConfigs("id1", classOf[DestructableComponent])
+
+ val container = newContainer(dirConfigSource)
+
+ val oldGraph = container.runOnce()
+ val componentToDestruct = oldGraph.getInstance(classOf[DestructableComponent])
+
+ writeBootstrapConfigs("id2", classOf[DestructableComponent])
+ container.reloadConfig(2)
+ container.runOnce(oldGraph)
+ assertTrue(componentToDestruct.deconstructed)
+ }
+
+ @Test
+ def previous_graph_is_retained_when_new_graph_throws_exception() {
+ val simpleComponentEntry = ComponentEntry("simpleComponent", classOf[SimpleComponent])
+
+ writeBootstrapConfigs(Array(simpleComponentEntry))
+ val container = newContainer(dirConfigSource)
+ var currentGraph = container.runOnce()
+
+ val simpleComponent = currentGraph.getInstance(classOf[SimpleComponent])
+
+ writeBootstrapConfigs("thrower", classOf[ComponentThrowingException])
+ dirConfigSource.writeConfig("test", """stringVal "myString" """)
+ container.reloadConfig(2)
+ try {
+ currentGraph = container.runOnce(currentGraph)
+ fail("Expected exception")
+ } catch {
+ case e: Exception => e.printStackTrace()
+ }
+
+ val componentTakingConfigEntry = ComponentEntry("componentTakingConfig", classOf[ComponentTakingConfig])
+ writeBootstrapConfigs(Array(simpleComponentEntry, componentTakingConfigEntry))
+ container.reloadConfig(3)
+ currentGraph = container.runOnce(currentGraph)
+
+ assertSame(simpleComponent, currentGraph.getInstance(classOf[SimpleComponent]))
+ assertNotNull(currentGraph.getInstance(classOf[ComponentTakingConfig]))
+ }
+
+ @Test
+ def runOnce_hangs_waiting_for_valid_config_after_invalid_config() {
+ dirConfigSource.writeConfig("test", """stringVal "original" """)
+ writeBootstrapConfigs("myId", classOf[ComponentTakingConfig])
+
+ val container = newContainer(dirConfigSource)
+ var currentGraph = container.runOnce()
+
+ writeBootstrapConfigs("thrower", classOf[ComponentThrowingException])
+ container.reloadConfig(2)
+
+ try {
+ currentGraph = container.runOnce(currentGraph)
+ fail("expected exception")
+ } catch {
+ case e: Exception =>
+ }
+
+ val newGraph = future {
+ currentGraph = container.runOnce(currentGraph)
+ currentGraph
+ }
+
+ Try {
+ Await.ready(newGraph, 1 second)
+ } foreach { x => fail("Expected waiting for new config.") }
+
+
+ writeBootstrapConfigs("myId2", classOf[ComponentTakingConfig])
+ container.reloadConfig(3)
+
+ assertNotNull(Await.result(newGraph, 5 minutes))
+ }
+
+
+ @Test
+ def bundle_info_is_set_on_rest_api_context() {
+ val clazz = classOf[RestApiContext]
+
+ writeBootstrapConfigs("restApiContext", clazz)
+ dirConfigSource.writeConfig("jersey-bundles", """bundles[0].spec "mock-entry-to-enforce-a-MockBundle" """)
+ dirConfigSource.writeConfig("jersey-injection", """inject[0]" """)
+
+ val container = newContainer(dirConfigSource)
+ val componentGraph = container.runOnce()
+
+ val restApiContext = componentGraph.getInstance(clazz)
+ assertNotNull(restApiContext)
+
+ assertThat(restApiContext.getBundles.size, is(1))
+ assertThat(restApiContext.getBundles.get(0).symbolicName, is(MockBundle.SymbolicName))
+ assertThat(restApiContext.getBundles.get(0).version, is(MockBundle.BundleVersion))
+
+ container.shutdownConfigurer()
+ }
+
+ @Test
+ def restApiContext_has_all_components_injected() {
+ new JerseyInjectionTest {
+ assertFalse(restApiContext.getInjectableComponents.isEmpty)
+ assertThat(restApiContext.getInjectableComponents.size(), is(2))
+
+ container.shutdownConfigurer()
+ }
+ }
+
+ // TODO: reuse injectedComponent as a named component when we support that
+ trait JerseyInjectionTest {
+ val restApiClass = classOf[RestApiContext]
+ val injectedClass = classOf[SimpleComponent]
+ val injectedComponentId = "injectedComponent"
+ val anotherComponentClass = classOf[SimpleComponent2]
+ val anotherComponentId = "anotherComponent"
+
+ val componentsConfig =
+ ComponentEntry(injectedComponentId, injectedClass).asConfig(0) + "\n" +
+ ComponentEntry(anotherComponentId, anotherComponentClass).asConfig(1) + "\n" +
+ ComponentEntry("restApiContext", restApiClass).asConfig(2) + "\n" +
+ s"components[2].inject[0].id $injectedComponentId\n" +
+ s"components[2].inject[1].id $anotherComponentId\n"
+
+ val injectionConfig = s"""inject[1]
+ |inject[0].instance $injectedComponentId
+ |inject[0].forClass "${injectedClass.getName}"
+ """.stripMargin
+
+ dirConfigSource.writeConfig("components", componentsConfig)
+ dirConfigSource.writeConfig("bundles", "")
+ dirConfigSource.writeConfig("jersey-bundles", """bundles[0].spec "mock-entry-to-enforce-a-MockBundle" """)
+ dirConfigSource.writeConfig("jersey-injection", injectionConfig)
+
+ val container = newContainer(dirConfigSource)
+ val componentGraph = container.runOnce()
+
+ val restApiContext = componentGraph.getInstance(restApiClass)
+ }
+
+ case class ComponentEntry(componentId: String, classId: Class[_]) {
+ def asConfig(position: Int) = {
+ <config>
+ |components[{position}].id "{componentId}"
+ |components[{position}].classId "{classId.getName}"
+ |components[{position}].configId "{dirConfigSource.configId}"
+ </config>.text.stripMargin.trim
+ }
+ }
+
+ def writeBootstrapConfigs(componentEntries: Array[ComponentEntry]) {
+ dirConfigSource.writeConfig("bundles", "")
+ dirConfigSource.writeConfig("components", """
+ components[%s]
+ %s
+ """.format(componentEntries.length,
+ componentEntries.zipWithIndex.map{ case (entry, index) => entry.asConfig(index) }.mkString("\n")))
+ }
+
+ def writeBootstrapConfigs(componentId: String = classOf[ComponentTakingConfig].getName,
+ classId: Class[_] = classOf[ComponentTakingConfig]) {
+
+ writeBootstrapConfigs(Array(ComponentEntry(componentId, classId)))
+ }
+
+ def writeBootstrapConfigsWithMultipleComponents(idAndClass: Array[(String, Class[_])]) {
+ writeBootstrapConfigs(idAndClass.map{case(id, classId) => ComponentEntry(id, classId)})
+ }
+
+
+ @Test
+ def providers_are_destructed() {
+ writeBootstrapConfigs("id1", classOf[DestructableProvider])
+
+ val deconstructor = new ComponentDeconstructor {
+ def deconstruct(component: AnyRef) {
+ component match {
+ case c : AbstractComponent => c.deconstruct()
+ case p : Provider[_] => p.deconstruct()
+ }
+ }
+ }
+
+ val container = newContainer(dirConfigSource, deconstructor)
+
+ val oldGraph = container.runOnce()
+ val destructableEntity = oldGraph.getInstance(classOf[DestructableEntity])
+
+ writeBootstrapConfigs("id2", classOf[DestructableProvider])
+ container.reloadConfig(2)
+ container.runOnce(oldGraph)
+
+ assertTrue(destructableEntity.deconstructed)
+ }
+}
+
+
+object ContainerTest {
+ class DestructableEntity {
+ var deconstructed = false
+ }
+
+ class DestructableProvider extends Provider[DestructableEntity] {
+ val instance = new DestructableEntity
+
+ def get() = instance
+
+ def deconstruct() {
+ require(instance.deconstructed == false)
+ instance.deconstructed = true
+ }
+ }
+
+ class ComponentTakingConfig(val config: TestConfig) extends AbstractComponent {
+ require(config != null)
+ }
+
+ class ComponentThrowingException(config:IntConfig) extends AbstractComponent {
+ throw new RuntimeException("This component can never be created")
+ }
+
+ class DestructableComponent extends AbstractComponent {
+ var deconstructed = false
+ override def deconstruct() {
+ deconstructed = true
+ }
+ }
+
+ class TestDeconstructor extends ComponentDeconstructor {
+ def deconstruct(component: AnyRef) {
+ component match {
+ case vespaComponent: DestructableComponent => vespaComponent.deconstruct()
+ case _ =>
+ }
+ }
+ }
+
+ private def newContainer(dirConfigSource: DirConfigSource,
+ deconstructor: ComponentDeconstructor = new TestDeconstructor()):
+ Container = {
+ new Container(new CloudSubscriberFactory(dirConfigSource.configSource), dirConfigSource.configId, deconstructor)
+ }
+
+ def createComponentTakingConfig(componentGraph: ComponentGraph): ComponentTakingConfig = {
+ componentGraph.getInstance(classOf[ComponentTakingConfig])
+ }
+
+ def convertMap[K, V](map: java.util.Map[K, V]) = JavaConversions.mapAsScalaMap(map).toMap
+}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala b/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala
new file mode 100644
index 00000000000..20e4fcb03f8
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import java.io.{FileOutputStream, OutputStream, File}
+import DirConfigSource._
+import java.util.Random
+import org.junit.rules.TemporaryFolder
+import com.yahoo.config.subscription.{ConfigSource, ConfigSourceSet}
+
+// TODO: Make this a junit rule. Does not yet work. Look out for junit updates
+// (@Rule def configSourceRule = dirConfigSource)
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+class DirConfigSource(val testSourcePrefix: String) {
+ private val tempFolder = createTemporaryFolder()
+
+ val configSource : ConfigSource = new ConfigSourceSet(testSourcePrefix + new Random().nextLong)
+
+ def writeConfig(name: String, contents: String) {
+ val file = new File(tempFolder.getRoot, name + ".cfg")
+ if (!file.exists())
+ file.createNewFile()
+
+ printFile(file, contents + "\n")
+ }
+
+ def configId = "dir:" + tempFolder.getRoot.getPath
+
+ def cleanup() {
+ tempFolder.delete()
+ }
+}
+
+private object DirConfigSource {
+ def printFile(f: File, content: String) {
+ var out: OutputStream = new FileOutputStream(f)
+ out.write(content.getBytes("UTF-8"))
+ out.close()
+ }
+
+ def createTemporaryFolder() = {
+ val folder = new TemporaryFolder
+ folder.create()
+ folder
+ }
+} \ No newline at end of file
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala
new file mode 100644
index 00000000000..43733923235
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala
@@ -0,0 +1,538 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core
+
+import org.junit.Test
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.{is, sameInstance, equalTo, not, containsString}
+
+import com.yahoo.component.provider.ComponentRegistry
+import com.google.inject.name.{Names, Named}
+import com.yahoo.component.{ComponentId, AbstractComponent}
+import org.hamcrest.Matcher
+import com.yahoo.vespa.config.ConfigKey
+import com.yahoo.config.{ConfigInstance}
+import com.yahoo.config.test.{TestConfig, Test2Config}
+import java.util.concurrent.{Executors, Executor}
+import com.google.inject.{Guice, Key, AbstractModule, Inject, Provider=>GuiceProvider}
+import com.yahoo.container.di._
+import com.yahoo.config.subscription.ConfigGetter
+import com.yahoo.container.di.config.{JerseyInjectionConfig, JerseyBundlesConfig, RestApiContext}
+import com.yahoo.container.di.componentgraph.Provider
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+class ComponentGraphTest {
+ import ComponentGraphTest._
+
+ private def keyAndConfig[T <: ConfigInstance](clazz: Class[T], configId: String): (ConfigKey[T], T) = {
+ val key = new ConfigKey(clazz, configId)
+ key -> ConfigGetter.getConfig(key.getConfigClass, key.getConfigId.toString)
+ }
+
+ @Test
+ def component_taking_config_can_be_instantiated() {
+ val componentGraph = new ComponentGraph
+ val configId = "raw:stringVal \"test-value\""
+ val componentNode = mockComponentNode(classOf[ComponentTakingConfig], configId)
+
+ componentGraph.add(componentNode)
+ componentGraph.complete()
+ componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[TestConfig], configId)))
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingConfig])
+ assertNotNull(instance)
+ assertThat(instance.config.stringVal(), is("test-value"))
+ }
+
+ @Test
+ def component_can_be_injected_into_another_component() {
+ val injectedComponent = mockComponentNode(classOf[SimpleComponent])
+ val targetComponent = mockComponentNode(classOf[ComponentTakingComponent])
+ targetComponent.inject(injectedComponent)
+
+ val destroyGlobalLookupComponent = mockComponentNode(classOf[SimpleComponent])
+
+ val componentGraph = new ComponentGraph
+ componentGraph.add(injectedComponent)
+ componentGraph.add(targetComponent)
+ componentGraph.add(destroyGlobalLookupComponent)
+ componentGraph.complete()
+
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingComponent])
+ assertNotNull(instance)
+ }
+
+ @Test
+ def all_components_of_a_type_can_be_injected() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents]))
+ componentGraph.complete()
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponents])
+ assertThat(instance.simpleComponents.allComponents().size(), is(3))
+ }
+
+ @Test
+ def empty_component_registry_can_be_injected() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents]))
+ componentGraph.complete()
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponents])
+ assertThat(instance.simpleComponents.allComponents().size(), is(0))
+ }
+
+ @Test
+ def component_registry_with_wildcard_upper_bound_can_be_injected() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponentsUpperBound]))
+ componentGraph.complete()
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponentsUpperBound])
+ assertThat(instance.simpleComponents.allComponents().size(), is(2))
+ }
+
+ @Test(expected = classOf[RuntimeException])
+ def require_exception_when_injecting_registry_with_unknown_type_variable() {
+ val clazz = classOf[ComponentTakingAllComponentsWithTypeVariable[_]]
+
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
+ componentGraph.add(mockComponentNode(clazz))
+ componentGraph.complete()
+
+ componentGraph.getInstance(clazz)
+ }
+
+ @Test
+ def components_are_shared() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.complete()
+
+ val instance1 = componentGraph.getInstance(classOf[SimpleComponent])
+ val instance2 = componentGraph.getInstance(classOf[SimpleComponent])
+ assertThat(instance1, sameInstance(instance2))
+ }
+
+ @Test
+ def singleton_components_can_be_injected() {
+ val componentGraph = new ComponentGraph
+ val configId = """raw:stringVal "test-value" """
+
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId))
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent2]))
+ componentGraph.complete()
+ componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[TestConfig], configId)))
+
+ val instance = componentGraph.getInstance(classOf[ComponentTakingComponent])
+ assertThat(instance.injectedComponent.asInstanceOf[ComponentTakingConfig].config.stringVal(), is("test-value"))
+ }
+
+ @Test(expected = classOf[RuntimeException])
+ def require_error_when_multiple_components_match_a_singleton_dependency() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
+ componentGraph.complete()
+ }
+
+ @Test
+ def named_component_can_be_injected() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent], key = Names.named("named-test")))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingNamedComponent]))
+ componentGraph.complete()
+ }
+
+ @Test
+ def config_keys_can_be_retrieved() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId = """raw:stringVal "component1" """""))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId = """raw:stringVal "component2" """""))
+ componentGraph.add(new ComponentRegistryNode(classOf[ComponentTakingConfig]))
+ componentGraph.complete()
+
+ val configKeys = componentGraph.configKeys
+ assertThat(configKeys.size, is(2))
+
+ configKeys.foreach{ key =>
+ assertThat(key.getConfigClass, equalTo(classOf[TestConfig]))
+ assertThat(key.getConfigId.toString, containsString("component"))
+ }
+ }
+
+ @Test
+ def providers_can_be_instantiated() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
+ componentGraph.complete()
+
+ assertNotNull(componentGraph.getInstance(classOf[Executor]))
+ }
+
+ @Test
+ def providers_can_be_inherited() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[DerivedExecutorProvider]))
+ componentGraph.complete()
+
+ assertNotNull(componentGraph.getInstance(classOf[Executor]))
+ }
+
+ @Test
+ def providers_can_deliver_a_new_instance_for_each_component() {
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNode(classOf[NewIntProvider]))
+ componentGraph.complete()
+
+ val instance1 = componentGraph.getInstance(classOf[Int])
+ val instance2 = componentGraph.getInstance(classOf[Int])
+ assertThat(instance1, not(equalTo(instance2)))
+ }
+
+ @Test
+ def providers_can_be_injected_explicitly() {
+ val componentGraph = new ComponentGraph
+
+ val componentTakingExecutor = mockComponentNode(classOf[ComponentTakingExecutor])
+ val executorProvider = mockComponentNode(classOf[ExecutorProvider])
+ componentTakingExecutor.inject(executorProvider)
+
+ componentGraph.add(executorProvider)
+ componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
+
+ componentGraph.add(componentTakingExecutor)
+
+ componentGraph.complete()
+ assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
+ }
+
+ @Test
+ def global_providers_can_be_injected() {
+ val componentGraph = new ComponentGraph
+
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
+ componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
+ componentGraph.add(mockComponentNode(classOf[IntProvider]))
+ componentGraph.complete()
+
+ assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
+ }
+
+ @Test(expected = classOf[RuntimeException])
+ def throw_if_multiple_global_providers_exist(): Unit = {
+ val componentGraph = new ComponentGraph
+
+ componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
+ componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
+ componentGraph.complete()
+ }
+
+ @Test
+ def provider_is_not_used_when_component_of_provided_class_exists() {
+ val componentGraph = new ComponentGraph
+
+ componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
+ componentGraph.add(mockComponentNode(classOf[SimpleComponentProviderThatThrows]))
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
+ componentGraph.complete()
+
+ val injectedComponent = componentGraph.getInstance(classOf[ComponentTakingComponent]).injectedComponent
+ assertNotNull(injectedComponent)
+ }
+
+ //TODO: move
+ @Test
+ def check_if_annotation_is_a_binding_annotation() {
+ import ComponentGraph.isBindingAnnotation
+
+ assertTrue(isBindingAnnotation(Names.named("name")))
+ assertFalse(isBindingAnnotation(classOf[Named].getAnnotations.head))
+ }
+
+ @Test
+ def cycles_gives_exception() {
+ val componentGraph = new ComponentGraph
+
+ def mockNode = mockComponentNode(classOf[ComponentCausingCycle])
+
+ val node1 = mockNode
+ val node2 = mockNode
+
+ node1.inject(node2)
+ node2.inject(node1)
+
+ componentGraph.add(node1)
+ componentGraph.add(node2)
+
+ try {
+ componentGraph.complete()
+ fail("Cycle exception expected.")
+ } catch {
+ case e : Throwable => assertThat(e.getMessage, containsString("cycle"))
+ }
+ }
+
+ @Test(expected = classOf[IllegalArgumentException])
+ def abstract_classes_are_rejected() {
+ new ComponentNode(ComponentId.fromString("Test"), "", classOf[AbstractClass])
+ }
+
+ @Test
+ def inject_constructor_is_preferred() {
+ assertThatComponentCanBeCreated(classOf[ComponentWithInjectConstructor])
+ }
+
+ @Test
+ def constructor_with_most_parameters_is_preferred() {
+ assertThatComponentCanBeCreated(classOf[ComponentWithMultipleConstructors])
+ }
+
+ def assertThatComponentCanBeCreated(clazz: Class[AnyRef]) {
+ val componentGraph = new ComponentGraph
+ val configId = """raw:stringVal "dummy" """"
+
+ componentGraph.add(mockComponentNode(clazz, configId))
+ componentGraph.complete()
+
+ componentGraph.setAvailableConfigs(Map(
+ keyAndConfig(classOf[TestConfig], configId),
+ keyAndConfig(classOf[Test2Config], configId)))
+
+ assertNotNull(componentGraph.getInstance(clazz))
+ }
+
+ @Test
+ def require_fallback_to_child_injector() {
+ val componentGraph = new ComponentGraph
+
+ componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
+
+ componentGraph.complete(singletonExecutorInjector)
+ assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
+ }
+
+ @Test
+ def child_injector_can_inject_multiple_instances_for_same_key() {
+ def executorProvider() = Executors.newSingleThreadExecutor()
+
+ val (graphSize, executorA, executorB) = buildGraphWithChildInjector(executorProvider)
+
+ assertThat(graphSize, is(4))
+ assertThat(executorA, not(sameInstance(executorB)))
+ }
+
+ @Test
+ def components_injected_via_child_injector_can_be_shared() {
+ val commonExecutor = Executors.newSingleThreadExecutor()
+ val (graphSize, executorA, executorB) = buildGraphWithChildInjector(() => commonExecutor)
+
+ assertThat(graphSize, is(3))
+ assertThat(executorA, sameInstance(executorB))
+ }
+
+ def buildGraphWithChildInjector(executorProvider: () => Executor) = {
+ val childInjector = Guice.createInjector(new AbstractModule {
+ override def configure() {
+ bind(classOf[Executor]).toProvider(new GuiceProvider[Executor] {
+ def get() = executorProvider()
+ })
+ }
+ })
+
+ val componentGraph = new ComponentGraph
+
+ def key(name: String) = Key.get(classOf[ComponentTakingExecutor], Names.named(name))
+ val keyA = key("A")
+ val keyB = key("B")
+
+ componentGraph.add(mockComponentNode(keyA))
+ componentGraph.add(mockComponentNode(keyB))
+
+ componentGraph.complete(childInjector)
+
+ (componentGraph.size, componentGraph.getInstance(keyA).executor, componentGraph.getInstance(keyB).executor)
+ }
+
+ @Test
+ def providers_can_be_reused() {
+ def createGraph() = {
+ val graph = new ComponentGraph()
+ graph.add(mockComponentNodeWithId(classOf[ExecutorProvider], "dummyId"))
+ graph.complete()
+ graph.setAvailableConfigs(Map())
+ graph
+ }
+
+ val oldGraph = createGraph()
+ val executor = oldGraph.getInstance(classOf[Executor])
+
+ val newGraph = createGraph()
+ newGraph.reuseNodes(oldGraph)
+
+ val newExecutor = newGraph.getInstance(classOf[Executor])
+ assertThat(executor, sameInstance(newExecutor))
+ }
+
+ @Test
+ def component_id_can_be_injected() {
+ val componentId: String = "myId:1.2@namespace"
+
+ val componentGraph = new ComponentGraph
+ componentGraph.add(mockComponentNodeWithId(classOf[ComponentTakingComponentId], componentId))
+ componentGraph.complete()
+
+ assertThat(componentGraph.getInstance(classOf[ComponentTakingComponentId]).componentId,
+ is(ComponentId.fromString(componentId)))
+ }
+
+ @Test
+ def rest_api_context_can_be_instantiated() {
+ val configId = """raw:"" """
+
+ val clazz = classOf[RestApiContext]
+ val jerseyNode = new JerseyNode(uniqueComponentId(clazz.getName), configId, clazz, new Osgi {})
+
+ val componentGraph = new ComponentGraph
+ componentGraph.add(jerseyNode)
+ componentGraph.complete()
+ componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[JerseyBundlesConfig], configId),
+ keyAndConfig(classOf[JerseyInjectionConfig], configId)))
+
+ val restApiContext = componentGraph.getInstance(clazz)
+ assertNotNull(restApiContext)
+ assertThat(restApiContext.getBundles.size, is(0))
+ }
+
+}
+
+//Note that all Components must be defined in a static context,
+//otherwise their constructor will take the outer class as the first parameter.
+object ComponentGraphTest {
+ var counter = 0
+
+
+ class SimpleComponent extends AbstractComponent
+ class SimpleComponent2 extends AbstractComponent
+ class SimpleDerivedComponent extends SimpleComponent
+
+ class ComponentTakingConfig(val config: TestConfig) extends SimpleComponent {
+ require(config != null)
+ }
+
+ class ComponentTakingComponent(val injectedComponent: SimpleComponent) extends AbstractComponent {
+ require(injectedComponent != null)
+ }
+
+ class ComponentTakingConfigAndComponent(val config: TestConfig, val injectedComponent: SimpleComponent) extends AbstractComponent {
+ require(config != null)
+ require(injectedComponent != null)
+ }
+
+ class ComponentTakingAllSimpleComponents(val simpleComponents: ComponentRegistry[SimpleComponent]) extends AbstractComponent {
+ require(simpleComponents != null)
+ }
+
+ class ComponentTakingAllSimpleComponentsUpperBound(val simpleComponents: ComponentRegistry[_ <: SimpleComponent])
+ extends AbstractComponent {
+
+ require(simpleComponents != null)
+ }
+
+ class ComponentTakingAllComponentsWithTypeVariable[COMPONENT <: AbstractComponent](val simpleComponents: ComponentRegistry[COMPONENT])
+ extends AbstractComponent {
+
+ require(simpleComponents != null)
+ }
+
+ class ComponentTakingNamedComponent(@Named("named-test") injectedComponent: SimpleComponent) extends AbstractComponent {
+ require(injectedComponent != null)
+ }
+
+ class ComponentCausingCycle(component: ComponentCausingCycle) extends AbstractComponent
+
+ class SimpleComponentProviderThatThrows extends Provider[SimpleComponent] {
+ def get() = throw new AssertionError("Should never be called.")
+ def deconstruct() {}
+ }
+
+ class ExecutorProvider extends Provider[Executor] {
+ val executor = Executors.newSingleThreadExecutor()
+ def get() = executor
+ def deconstruct() { /*TODO */ }
+ }
+
+ class DerivedExecutorProvider extends ExecutorProvider
+
+ class IntProvider extends Provider[java.lang.Integer] {
+ def get() = throw new AssertionError("Should never be called.")
+ def deconstruct() {}
+ }
+
+ class NewIntProvider extends Provider[Integer] {
+ var i: Int = 0
+ def get() = {
+ i += 1
+ i
+ }
+ def deconstruct() {}
+ }
+
+ class ComponentTakingExecutor(val executor: Executor) extends AbstractComponent {
+ require(executor != null)
+ }
+
+ class ComponentWithInjectConstructor private () {
+ def this(c: TestConfig, c2: Test2Config) = { this(); sys.error("Should not be called") }
+ @Inject
+ def this(c: Test2Config) = { this() }
+ }
+
+ class ComponentWithMultipleConstructors private (dummy : Int) {
+ def this(c: TestConfig, c2: Test2Config) = { this(0); }
+
+ def this() = { this(0); sys.error("Should not be called") }
+ def this(c: Test2Config) = { this() }
+ }
+
+ class ComponentTakingComponentId(val componentId: ComponentId)
+
+ def uniqueComponentId(className: String): ComponentId = {
+ counter += 1
+ ComponentId.fromString(className + counter)
+ }
+
+ def mockComponentNode(key: Key[_ <: AnyRef]): Node =
+ mockComponentNode(key.getTypeLiteral.getRawType.asInstanceOf[Class[AnyRef]], key=key.getAnnotation)
+
+ def mockComponentNode(clazz: Class[_ <: AnyRef], configId: String = "", key: JavaAnnotation = null): Node =
+ new ComponentNode(uniqueComponentId(clazz.getName), configId, clazz, key)
+
+ def mockComponentNodeWithId(clazz: Class[_ <: AnyRef], componentId: String, configId: String = "", key: JavaAnnotation = null): Node =
+ new ComponentNode(ComponentId.fromString(componentId), configId, clazz, key)
+
+ val singletonExecutorInjector = Guice.createInjector(new AbstractModule {
+ override def configure() {
+ bind(classOf[Executor]).toInstance(Executors.newSingleThreadExecutor())
+ }
+ })
+
+ implicit def makeMatcherCovariant[T, U >: T](matcher: Matcher[T]) : Matcher[U] = matcher.asInstanceOf[Matcher[U]]
+
+ abstract class AbstractClass
+}
+
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala
new file mode 100644
index 00000000000..fcbde13639d
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core
+
+import java.util
+import java.util.Collections
+import com.yahoo.container.di.osgi.OsgiUtil
+import org.junit.Test
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.is
+import org.hamcrest.Matchers.{contains, containsInAnyOrder}
+import org.osgi.framework.wiring.BundleWiring
+import scala.collection.JavaConverters._
+import com.yahoo.container.bundle.MockBundle
+
+/**
+ *
+ * @author gjoranv
+ * @since 5.17
+ */
+
+class JerseyNodeTest {
+
+ trait WithMockBundle {
+ object bundle extends MockBundle {
+ val entry = Map(
+ "com/foo" -> "Foo.class",
+ "com/bar" -> "Bar.class)"
+ ) map { case (packageName, className) => (packageName, packageName + "/" + className)}
+
+
+ override def listResources(path: String, ignored: String, options: Int): util.Collection[String] = {
+ if ((options & BundleWiring.LISTRESOURCES_RECURSE) != 0 && path == "/") entry.values.asJavaCollection
+ else Collections.singleton(entry(path))
+ }
+ }
+
+ val bundleClasses = bundle.entry.values.toList
+ }
+
+ @Test
+ def all_bundle_entries_are_returned_when_no_packages_are_given() {
+ new WithMockBundle {
+ val entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Set()).asJavaCollection
+ assertThat(entries, containsInAnyOrder(bundleClasses: _*))
+ }
+ }
+
+ @Test
+ def only_bundle_entries_from_the_given_packages_are_returned() {
+ new WithMockBundle {
+ val entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Set("com.foo")).asJavaCollection
+ assertThat(entries, contains(bundle.entry("com/foo")))
+ }
+ }
+
+ @Test
+ def bundle_info_is_initialized() {
+ new WithMockBundle {
+ val bundleInfo = JerseyNode.createBundleInfo(bundle, List())
+ assertThat(bundleInfo.symbolicName, is(bundle.getSymbolicName))
+ assertThat(bundleInfo.version, is(bundle.getVersion))
+ assertThat(bundleInfo.fileLocation, is(bundle.getLocation))
+ }
+ }
+
+}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala
new file mode 100644
index 00000000000..e9c0be03a30
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala
@@ -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.container.di.componentgraph.core
+
+import com.yahoo.component.{ComponentId, AbstractComponent}
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.{is, not, sameInstance, equalTo}
+import com.yahoo.vespa.config.ConfigKey
+import java.util.concurrent.Executor
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{ExecutorProvider, SimpleComponent, SimpleComponent2}
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{ComponentTakingConfig, ComponentTakingExecutor, singletonExecutorInjector}
+import com.yahoo.container.di.makeClassCovariant
+import org.junit.Test
+import com.yahoo.config.subscription.ConfigGetter
+import com.yahoo.config.test.TestConfig
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+class ReuseComponentsTest {
+ import ReuseComponentsTest._
+
+ @Test
+ def require_that_component_is_reused_when_componentNode_is_unmodified() {
+ def reuseAndTest(classToRegister: Class[AnyRef], classToLookup: Class[AnyRef]) {
+ val graph = buildGraphAndSetNoConfigs(classToRegister)
+ val instance = getComponent(graph, classToLookup)
+
+ val newGraph = buildGraphAndSetNoConfigs(classToRegister)
+ newGraph.reuseNodes(graph)
+ val instance2 = getComponent(newGraph, classToLookup)
+
+ assertThat(instance2, sameInstance(instance))
+ }
+
+ reuseAndTest(classOf[SimpleComponent], classOf[SimpleComponent])
+ reuseAndTest(classOf[ExecutorProvider], classOf[Executor])
+ }
+
+
+ @Test(expected = classOf[IllegalStateException])
+ def require_that_component_is_not_reused_when_class_is_changed() {
+ val graph = buildGraphAndSetNoConfigs(classOf[SimpleComponent])
+ val instance = getComponent(graph, classOf[SimpleComponent])
+
+ val newGraph = buildGraphAndSetNoConfigs(classOf[SimpleComponent2])
+ newGraph.reuseNodes(graph)
+ val instance2 = getComponent(newGraph, classOf[SimpleComponent2])
+
+ assertThat(instance2.getId, is(instance.getId))
+ val throwsException = getComponent(newGraph, classOf[SimpleComponent])
+ }
+
+ @Test
+ def require_that_component_is_not_reused_when_config_is_changed() {
+ def setConfig(graph: ComponentGraph, config: String) {
+ graph.setAvailableConfigs(
+ Map(new ConfigKey(classOf[TestConfig], "component") ->
+ ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
+ }
+
+ val componentClass = classOf[ComponentTakingConfig]
+
+ val graph = buildGraph(componentClass)
+ setConfig(graph, "oldConfig")
+ val instance = getComponent(graph, componentClass)
+
+ val newGraph = buildGraph(componentClass)
+ setConfig(newGraph, "newConfig")
+ newGraph.reuseNodes(graph)
+ val instance2 = getComponent(newGraph, componentClass)
+
+ assertThat(instance2, not(sameInstance(instance)))
+ }
+
+ @Test
+ def require_that_component_is_not_reused_when_injected_component_is_changed() {
+ import ComponentGraphTest.{ComponentTakingComponent, ComponentTakingConfig}
+
+ def buildGraph(config: String) = {
+ val graph = new ComponentGraph
+
+ val rootComponent = mockComponentNode(classOf[ComponentTakingComponent], "root_component")
+
+ val configId = "componentTakingConfigId"
+ val injectedComponent = mockComponentNode(classOf[ComponentTakingConfig], "injected_component", configId)
+
+ rootComponent.inject(injectedComponent)
+
+ graph.add(rootComponent)
+ graph.add(injectedComponent)
+
+ graph.complete()
+ graph.setAvailableConfigs(Map(new ConfigKey(classOf[TestConfig], configId) ->
+ ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
+
+ graph
+ }
+
+ val oldGraph = buildGraph(config="oldGraph")
+ val oldInstance = getComponent(oldGraph, classOf[ComponentTakingComponent])
+
+ val newGraph = buildGraph(config="newGraph")
+ newGraph.reuseNodes(oldGraph)
+ val newInstance = getComponent(newGraph, classOf[ComponentTakingComponent])
+
+ assertThat(newInstance, not(sameInstance(oldInstance)))
+ }
+
+ @Test
+ def require_that_component_is_not_reused_when_injected_component_registry_has_one_component_removed() {
+ import ComponentGraphTest.ComponentTakingAllSimpleComponents
+
+ def buildGraph(useBothInjectedComponents: Boolean) = {
+ val graph = new ComponentGraph
+ graph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents], "root_component"))
+
+ /* Below if-else has code duplication, but explicit ordering of the two components
+ * was necessary to reproduce erroneous behaviour in ComponentGraph.reuseNodes that
+ * occurred before ComponentRegistryNode got its own 'equals' implementation.
+ */
+ if (useBothInjectedComponents) {
+ graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component2"))
+ graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component1"))
+ } else {
+ graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component1"))
+ }
+
+ graph.complete()
+ graph.setAvailableConfigs(Map())
+ graph
+ }
+
+ val oldGraph = buildGraph(useBothInjectedComponents = true)
+ val oldSimpleComponentRegistry = getComponent(oldGraph, classOf[ComponentTakingAllSimpleComponents]).simpleComponents
+
+ val newGraph = buildGraph(useBothInjectedComponents = false)
+ newGraph.reuseNodes(oldGraph)
+ val newSimpleComponentRegistry = getComponent(newGraph, classOf[ComponentTakingAllSimpleComponents]).simpleComponents
+
+ assertThat(newSimpleComponentRegistry, not(sameInstance(oldSimpleComponentRegistry)))
+ }
+
+ @Test
+ def require_that_injected_component_is_reused_even_when_dependent_component_is_changed() {
+ import ComponentGraphTest.{ComponentTakingConfigAndComponent, SimpleComponent}
+
+ def buildGraph(config: String) = {
+ val graph = new ComponentGraph
+
+ val configId = "componentTakingConfigAndComponent"
+ val rootComponent = mockComponentNode(classOf[ComponentTakingConfigAndComponent], "root_component", configId)
+
+ val injectedComponent = mockComponentNode(classOf[SimpleComponent], "injected_component")
+
+ rootComponent.inject(injectedComponent)
+
+ graph.add(rootComponent)
+ graph.add(injectedComponent)
+
+ graph.complete()
+ graph.setAvailableConfigs(Map(new ConfigKey(classOf[TestConfig], configId) ->
+ ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
+
+ graph
+ }
+
+ val oldGraph = buildGraph(config="oldGraph")
+ val oldInjectedComponent = getComponent(oldGraph, classOf[SimpleComponent])
+ val oldDependentComponent = getComponent(oldGraph, classOf[ComponentTakingConfigAndComponent])
+
+ val newGraph = buildGraph(config="newGraph")
+ newGraph.reuseNodes(oldGraph)
+ val newInjectedComponent = getComponent(newGraph, classOf[SimpleComponent])
+ val newDependentComponent = getComponent(newGraph, classOf[ComponentTakingConfigAndComponent])
+
+ assertThat(newDependentComponent, not(sameInstance(oldDependentComponent)))
+ assertThat(newInjectedComponent, sameInstance(oldInjectedComponent))
+ }
+
+ @Test
+ def require_that_node_depending_on_guice_node_is_reused() {
+ def makeGraph = {
+ val graph = new ComponentGraph
+ graph.add(mockComponentNode(classOf[ComponentTakingExecutor], "dummyId"))
+ graph.complete(singletonExecutorInjector)
+ graph.setAvailableConfigs(Map())
+ graph
+ }
+
+ val getComponentTakingExecutor = getComponent(_: ComponentGraph, classOf[ComponentTakingExecutor])
+
+ val oldGraph = makeGraph
+ getComponentTakingExecutor(oldGraph) // Ensure creation of GuiceNode
+ val newGraph = makeGraph
+ newGraph.reuseNodes(oldGraph)
+ assertThat(getComponentTakingExecutor(oldGraph), sameInstance(getComponentTakingExecutor(newGraph)))
+ }
+
+ @Test
+ def require_that_node_equals_only_checks_first_level_components_to_inject() {
+
+ def createNodeWithInjectedNodeWithInjectedNode(indirectlyInjectedComponentId: String): Node = {
+ val targetComponent = mockComponentNode(classOf[SimpleComponent], "target")
+ val directlyInjectedComponent = mockComponentNode(classOf[SimpleComponent], "directlyInjected")
+ val indirectlyInjectedComponent = mockComponentNode(classOf[SimpleComponent], indirectlyInjectedComponentId)
+ directlyInjectedComponent.inject(indirectlyInjectedComponent)
+ targetComponent.inject(directlyInjectedComponent)
+
+ completeNode(targetComponent)
+ completeNode(directlyInjectedComponent)
+ completeNode(indirectlyInjectedComponent)
+
+ targetComponent
+ }
+ val targetNode1 = createNodeWithInjectedNodeWithInjectedNode("indirectlyInjected_1")
+ val targetNode2 = createNodeWithInjectedNodeWithInjectedNode("indirectlyInjected_2")
+ assertThat(targetNode1, equalTo(targetNode2))
+ }
+
+ private def completeNode(node: ComponentNode) {
+ node.setArguments(Array())
+ node.setAvailableConfigs(Map())
+ }
+
+ private def buildGraph(componentClass: Class[_ <: AnyRef]) = {
+ val commonComponentId = "component"
+ val g = new ComponentGraph
+ g.add(mockComponentNode(componentClass, commonComponentId, configId = commonComponentId))
+ g.complete()
+ g
+ }
+
+ private def buildGraphAndSetNoConfigs(componentClass: Class[_ <: AnyRef]) = {
+ val g = buildGraph(componentClass)
+ g.setAvailableConfigs(Map())
+ g
+ }
+}
+
+object ReuseComponentsTest {
+
+ def mockComponentNode(clazz: Class[_ <: AnyRef], componentId: String = "", configId: String="") =
+ new ComponentNode(new ComponentId(componentId), configId, clazz)
+
+ def getComponent[T](graph: ComponentGraph, clazz: Class[T]) = {
+ graph.getInstance(clazz)
+ }
+}
diff --git a/container-di/src/test/vespa-configdef/bootstrap1.def b/container-di/src/test/vespa-configdef/bootstrap1.def
new file mode 100644
index 00000000000..3af0c945db8
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/bootstrap1.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
diff --git a/container-di/src/test/vespa-configdef/bootstrap2.def b/container-di/src/test/vespa-configdef/bootstrap2.def
new file mode 100644
index 00000000000..ba0e8cccd56
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/bootstrap2.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
+
diff --git a/container-di/src/test/vespa-configdef/components1.def b/container-di/src/test/vespa-configdef/components1.def
new file mode 100644
index 00000000000..3af0c945db8
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/components1.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
diff --git a/container-di/src/test/vespa-configdef/int.def b/container-di/src/test/vespa-configdef/int.def
new file mode 100644
index 00000000000..99fb79d64cf
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/int.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.di
+
+intVal int default=1
diff --git a/container-di/src/test/vespa-configdef/string.def b/container-di/src/test/vespa-configdef/string.def
new file mode 100644
index 00000000000..53585338ee6
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/string.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.di
+
+stringVal string default="_default_"
diff --git a/container-di/src/test/vespa-configdef/test.def b/container-di/src/test/vespa-configdef/test.def
new file mode 100644
index 00000000000..d77b2e9df16
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/test.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+stringVal string default="default"
diff --git a/container-di/src/test/vespa-configdef/test2.def b/container-di/src/test/vespa-configdef/test2.def
new file mode 100644
index 00000000000..d77b2e9df16
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/test2.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+stringVal string default="default"
diff --git a/container-di/src/test/vespa-configdef/thread-pool.def b/container-di/src/test/vespa-configdef/thread-pool.def
new file mode 100644
index 00000000000..31741e913b6
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/thread-pool.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+numThreads int