diff options
author | Harald Musum <musum@verizonmedia.com> | 2021-04-06 14:19:35 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-06 14:19:35 +0200 |
commit | 778894b29b13831115c19ff13285541a10ab2d30 (patch) | |
tree | 62ea7aa8689a3d5421f54cd0ac6c5290e82f23dc /container-di/src/test | |
parent | 5df00bb90a04082847440716bcb6146bdda0ca06 (diff) |
Revert "Gjoranv/merge di into core (rebased)"
Diffstat (limited to 'container-di/src/test')
21 files changed, 2356 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..6fe58e99fda --- /dev/null +++ b/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java @@ -0,0 +1,83 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.component; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Arne Bergene Fossaa + */ +public class ComponentSpecTestCase { + + @Test + 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)); + + } + + @Test + 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)); + } + + @Test + 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..69eec95b746 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java @@ -0,0 +1,94 @@ +// Copyright 2017 Yahoo Holdings. 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 Tony Vaagenes + */ +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..0842ee4a797 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java @@ -0,0 +1,43 @@ +// Copyright 2017 Yahoo Holdings. 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 org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author bratseth + */ +public class ComponentIdTestCase { + + @Test + 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()); + } + + @Test + 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/com/yahoo/container/di/ConfigRetrieverTest.java b/container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java new file mode 100644 index 00000000000..290836d7842 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java @@ -0,0 +1,121 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.test.Bootstrap1Config; +import com.yahoo.config.test.Bootstrap2Config; +import com.yahoo.config.test.TestConfig; +import com.yahoo.container.di.ConfigRetriever.BootstrapConfigs; +import com.yahoo.container.di.ConfigRetriever.ComponentsConfigs; +import com.yahoo.container.di.ConfigRetriever.ConfigSnapshot; +import com.yahoo.vespa.config.ConfigKey; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * + * @author gjoranv + * @author Tony Vaagenes + * @author ollivir + */ +public class ConfigRetrieverTest { + + private DirConfigSource dirConfigSource = null; + + @Before + public void setup() { + dirConfigSource = new DirConfigSource("ConfigRetrieverTest-"); + } + + @After + public void cleanup() { + dirConfigSource.cleanup(); + } + + @Test + public void require_that_bootstrap_configs_come_first() { + writeConfigs(); + ConfigRetriever retriever = createConfigRetriever(); + ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, true); + + assertThat(bootstrapConfigs, Matchers.instanceOf(BootstrapConfigs.class)); + } + + @Test + @SuppressWarnings("unused") + public void require_that_components_comes_after_bootstrap() { + writeConfigs(); + ConfigRetriever retriever = createConfigRetriever(); + ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, true); + + ConfigKey<? extends ConfigInstance> testConfigKey = new ConfigKey<>(TestConfig.class, dirConfigSource.configId()); + ConfigSnapshot componentsConfigs = retriever.getConfigs(Collections.singleton(testConfigKey), 0, true); + + if (componentsConfigs instanceof ComponentsConfigs) { + assertThat(componentsConfigs.size(), is(3)); + } else { + fail("ComponentsConfigs has unexpected type: " + componentsConfigs); + } + } + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Ignore + @SuppressWarnings("unused") + public void require_exception_upon_modified_components_keys_without_bootstrap() { + expectedException.expect(IllegalArgumentException.class); + + writeConfigs(); + ConfigRetriever retriever = createConfigRetriever(); + ConfigKey<? extends ConfigInstance> testConfigKey = new ConfigKey<>(TestConfig.class, dirConfigSource.configId()); + ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, true); + ConfigSnapshot componentsConfigs = retriever.getConfigs(Collections.singleton(testConfigKey), 0, true); + Set<ConfigKey<? extends ConfigInstance>> keys = new HashSet<>(); + keys.add(testConfigKey); + keys.add(new ConfigKey<>(TestConfig.class, "")); + retriever.getConfigs(keys, 0, true); + } + + @Test + public void require_that_empty_components_keys_after_bootstrap_returns_components_configs() { + writeConfigs(); + ConfigRetriever retriever = createConfigRetriever(); + assertThat(retriever.getConfigs(Collections.emptySet(), 0, true), instanceOf(BootstrapConfigs.class)); + assertThat(retriever.getConfigs(Collections.emptySet(), 0, true), instanceOf(ComponentsConfigs.class)); + } + + public void writeConfigs() { + writeConfig("bootstrap1", "dummy \"ignored\""); + writeConfig("bootstrap2", "dummy \"ignored\""); + writeConfig("test", "stringVal \"ignored\""); + } + + private ConfigRetriever createConfigRetriever() { + String configId = dirConfigSource.configId(); + CloudSubscriberFactory subscriber = new CloudSubscriberFactory(dirConfigSource.configSource()); + Set<ConfigKey<? extends ConfigInstance>> keys = new HashSet<>(); + keys.add(new ConfigKey<>(Bootstrap1Config.class, configId)); + keys.add(new ConfigKey<>(Bootstrap2Config.class, configId)); + return new ConfigRetriever(keys, keySet -> subscriber.getSubscriber(keySet)); + } + + private void writeConfig(String name, String contents) { + dirConfigSource.writeConfig(name, contents); + } +} diff --git a/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java b/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java new file mode 100644 index 00000000000..b596246a43d --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java @@ -0,0 +1,408 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di; + +import com.google.inject.Guice; +import com.yahoo.component.AbstractComponent; +import com.yahoo.config.di.IntConfig; +import com.yahoo.config.test.TestConfig; +import com.yahoo.container.bundle.MockBundle; +import com.yahoo.container.di.componentgraph.Provider; +import com.yahoo.container.di.componentgraph.core.ComponentGraph; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent2; +import com.yahoo.container.di.componentgraph.core.ComponentNode.ComponentConstructorException; +import com.yahoo.container.di.config.RestApiContext; +import org.junit.Ignore; +import org.junit.Test; +import org.osgi.framework.Bundle; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutorService; +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 Tony Vaagenes + * @author gjoranv + * @author ollivir + */ +public class ContainerTest extends ContainerTestBase { + + @Test + public void components_can_be_created_from_config() { + writeBootstrapConfigs(); + dirConfigSource.writeConfig("test", "stringVal \"myString\""); + + Container container = newContainer(dirConfigSource); + + ComponentTakingConfig component = createComponentTakingConfig(getNewComponentGraph(container)); + assertEquals("myString", component.config.stringVal()); + + container.shutdownConfigurer(); + } + + @Test + public void components_are_reconfigured_after_config_update_without_bootstrap_configs() { + writeBootstrapConfigs(); + dirConfigSource.writeConfig("test", "stringVal \"original\""); + + Container container = newContainer(dirConfigSource); + ComponentGraph componentGraph = getNewComponentGraph(container); + ComponentTakingConfig component = createComponentTakingConfig(componentGraph); + + assertEquals("original", component.config.stringVal()); + + // Reconfigure + dirConfigSource.writeConfig("test", "stringVal \"reconfigured\""); + container.reloadConfig(2); + + ComponentGraph newComponentGraph = getNewComponentGraph(container, componentGraph); + ComponentTakingConfig component2 = createComponentTakingConfig(newComponentGraph); + assertEquals("reconfigured", component2.config.stringVal()); + + container.shutdownConfigurer(); + } + + @Test + public void graph_is_updated_after_bootstrap_update() { + dirConfigSource.writeConfig("test", "stringVal \"original\""); + writeBootstrapConfigs("id1"); + + Container container = newContainer(dirConfigSource); + + ComponentGraph graph = getNewComponentGraph(container); + ComponentTakingConfig component = createComponentTakingConfig(graph); + assertEquals("id1", component.getId().toString()); + + writeBootstrapConfigs( + new ComponentEntry("id1", ComponentTakingConfig.class), + new ComponentEntry("id2", ComponentTakingConfig.class)); + + container.reloadConfig(2); + ComponentGraph newGraph = getNewComponentGraph(container, graph); + + assertNotNull(ComponentGraph.getNode(newGraph, "id1")); + assertNotNull(ComponentGraph.getNode(newGraph, "id2")); + + container.shutdownConfigurer(); + } + + //@Test TODO + public void deconstructor_is_given_guice_components() { + } + + @Test + public void component_is_deconstructed_when_not_reused() { + writeBootstrapConfigs("id1", DestructableComponent.class); + + Container container = newContainer(dirConfigSource); + + ComponentGraph oldGraph = getNewComponentGraph(container); + DestructableComponent componentToDestruct = oldGraph.getInstance(DestructableComponent.class); + + writeBootstrapConfigs("id2", DestructableComponent.class); + container.reloadConfig(2); + getNewComponentGraph(container, oldGraph); + assertTrue(componentToDestruct.deconstructed); + } + + @Ignore // because logAndDie is impossible(?) to verify programmatically + @Test + public void manually_verify_what_happens_when_first_graph_contains_component_that_throws_exception_in_ctor() { + writeBootstrapConfigs("thrower", ComponentThrowingExceptionInConstructor.class); + Container container = newContainer(dirConfigSource); + try { + getNewComponentGraph(container); + fail("Expected to log and die."); + } catch (Throwable t) { + fail("Expected to log and die"); + } + } + + @Test + public void previous_graph_is_retained_when_new_graph_contains_component_that_throws_exception_in_ctor() { + ComponentEntry simpleComponentEntry = new ComponentEntry("simpleComponent", SimpleComponent.class); + + writeBootstrapConfigs(simpleComponentEntry); + Container container = newContainer(dirConfigSource); + ComponentGraph currentGraph = getNewComponentGraph(container); + + SimpleComponent simpleComponent = currentGraph.getInstance(SimpleComponent.class); + + writeBootstrapConfigs("thrower", ComponentThrowingExceptionInConstructor.class); + container.reloadConfig(2); + try { + currentGraph = getNewComponentGraph(container, currentGraph); + fail("Expected exception"); + } catch (ComponentConstructorException ignored) { + // Expected, do nothing + } catch (Throwable t) { + fail("Expected ComponentConstructorException"); + } + assertEquals(1, currentGraph.generation()); + + // Also verify that next reconfig is successful + ComponentEntry componentTakingConfigEntry = new ComponentEntry("componentTakingConfig", ComponentTakingConfig.class); + dirConfigSource.writeConfig("test", "stringVal \"myString\""); + writeBootstrapConfigs(simpleComponentEntry, componentTakingConfigEntry); + container.reloadConfig(3); + currentGraph = getNewComponentGraph(container, currentGraph); + + assertEquals(3, currentGraph.generation()); + assertSame(simpleComponent, currentGraph.getInstance(SimpleComponent.class)); + assertNotNull(currentGraph.getInstance(ComponentTakingConfig.class)); + } + + @Test + public void previous_graph_is_retained_when_new_graph_throws_exception_for_missing_config() { + ComponentEntry simpleComponentEntry = new ComponentEntry("simpleComponent", SimpleComponent.class); + + writeBootstrapConfigs(simpleComponentEntry); + Container container = newContainer(dirConfigSource); + ComponentGraph currentGraph = getNewComponentGraph(container); + + currentGraph.getInstance(SimpleComponent.class); + + writeBootstrapConfigs("thrower", ComponentThrowingExceptionForMissingConfig.class); + dirConfigSource.writeConfig("test", "stringVal \"myString\""); + container.reloadConfig(2); + try { + currentGraph = getNewComponentGraph(container, currentGraph); + fail("Expected exception"); + } catch (IllegalArgumentException ignored) { + // Expected, do nothing + } catch (Throwable t) { + fail("Expected IllegalArgumentException"); + } + assertEquals(1, currentGraph.generation()); + } + + @Test + public void getNewComponentGraph_hangs_waiting_for_valid_config_after_invalid_config() throws Exception { + dirConfigSource.writeConfig("test", "stringVal \"original\""); + writeBootstrapConfigs("myId", ComponentTakingConfig.class); + + Container container = newContainer(dirConfigSource); + final ComponentGraph currentGraph = getNewComponentGraph(container); + + writeBootstrapConfigs("thrower", ComponentThrowingExceptionForMissingConfig.class); + container.reloadConfig(2); + + try { + getNewComponentGraph(container, currentGraph); + fail("expected exception"); + } catch (Exception ignored) { + } + ExecutorService exec = Executors.newFixedThreadPool(1); + Future<ComponentGraph> newGraph = exec.submit(() -> getNewComponentGraph(container, currentGraph)); + + try { + newGraph.get(1, TimeUnit.SECONDS); + fail("Expected waiting for new config."); + } catch (Exception ignored) { + // expect to time out + } + + writeBootstrapConfigs("myId2", ComponentTakingConfig.class); + container.reloadConfig(3); + + assertNotNull(newGraph.get(5, TimeUnit.MINUTES)); + } + + + @Test + public void bundle_info_is_set_on_rest_api_context() { + Class<RestApiContext> clazz = RestApiContext.class; + + writeBootstrapConfigs("restApiContext", clazz); + dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\""); + dirConfigSource.writeConfig("jersey-injection", "inject[0]"); + + Container container = newContainer(dirConfigSource); + ComponentGraph componentGraph = getNewComponentGraph(container); + + RestApiContext restApiContext = componentGraph.getInstance(clazz); + assertNotNull(restApiContext); + + assertEquals(1, restApiContext.getBundles().size()); + assertEquals(MockBundle.SymbolicName, restApiContext.getBundles().get(0).symbolicName); + assertEquals(MockBundle.BundleVersion, restApiContext.getBundles().get(0).version); + + container.shutdownConfigurer(); + } + + @Test + public void restApiContext_has_all_components_injected() { + Class<RestApiContext> restApiClass = RestApiContext.class; + Class<SimpleComponent> injectedClass = SimpleComponent.class; + String injectedComponentId = "injectedComponent"; + Class<SimpleComponent2> anotherComponentClass = SimpleComponent2.class; + String anotherComponentId = "anotherComponent"; + + String componentsConfig = + new ComponentEntry(injectedComponentId, injectedClass).asConfig(0) + "\n" + + new ComponentEntry(anotherComponentId, anotherComponentClass).asConfig(1) + "\n" + + new ComponentEntry("restApiContext", restApiClass).asConfig(2) + "\n" + + "components[2].inject[0].id " + injectedComponentId + "\n" + + "components[2].inject[1].id " + anotherComponentId + "\n"; + + String injectionConfig = "inject[1]\n" +// + "inject[0].instance " + injectedComponentId + "\n" +// + "inject[0].forClass \"" + injectedClass.getName() + "\"\n"; + + dirConfigSource.writeConfig("components", componentsConfig); + dirConfigSource.writeConfig("platform-bundles", ""); + dirConfigSource.writeConfig("application-bundles", ""); + dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\""); + dirConfigSource.writeConfig("jersey-injection", injectionConfig); + + Container container = newContainer(dirConfigSource); + ComponentGraph componentGraph = getNewComponentGraph(container); + + RestApiContext restApiContext = componentGraph.getInstance(restApiClass); + + assertFalse(restApiContext.getInjectableComponents().isEmpty()); + assertEquals(2, restApiContext.getInjectableComponents().size()); + + container.shutdownConfigurer(); + } + + @Test + public void providers_are_destructed() { + writeBootstrapConfigs("id1", DestructableProvider.class); + + ComponentDeconstructor deconstructor = (components, bundles) -> { + components.forEach(component -> { + if (component instanceof AbstractComponent) { + ((AbstractComponent) component).deconstruct(); + } else if (component instanceof Provider) { + ((Provider<?>) component).deconstruct(); + } + }); + if (! bundles.isEmpty()) throw new IllegalArgumentException("This test should not use bundles"); + }; + + Container container = newContainer(dirConfigSource, deconstructor); + + ComponentGraph oldGraph = getNewComponentGraph(container); + DestructableEntity destructableEntity = oldGraph.getInstance(DestructableEntity.class); + + writeBootstrapConfigs("id2", DestructableProvider.class); + container.reloadConfig(2); + getNewComponentGraph(container, oldGraph); + + assertTrue(destructableEntity.deconstructed); + } + + @Test + public void providers_are_invoked_only_when_needed() { + writeBootstrapConfigs("id1", FailOnGetProvider.class); + + Container container = newContainer(dirConfigSource); + + ComponentGraph oldGraph = getNewComponentGraph(container); + } + + static class DestructableEntity { + private boolean deconstructed = false; + } + + public static class DestructableProvider implements Provider<DestructableEntity> { + DestructableEntity instance = new DestructableEntity(); + + public DestructableEntity get() { + return instance; + } + + public void deconstruct() { + assertFalse(instance.deconstructed); + instance.deconstructed = true; + } + } + + public static class FailOnGetProvider implements Provider<Integer> { + + public Integer get() { + fail("Should never be called."); + return null; + } + + public void deconstruct() { + } + + } + + public static class ComponentTakingConfig extends AbstractComponent { + private final TestConfig config; + + public ComponentTakingConfig(TestConfig config) { + assertNotNull(config); + this.config = config; + } + } + + public static class ComponentThrowingExceptionInConstructor { + public ComponentThrowingExceptionInConstructor() { + throw new RuntimeException("This component fails upon construction."); + } + } + + public static class ComponentThrowingExceptionForMissingConfig extends AbstractComponent { + public ComponentThrowingExceptionForMissingConfig(IntConfig intConfig) { + fail("This component should never be created. Only used for tests where 'int' config is missing."); + } + } + + public static class DestructableComponent extends AbstractComponent { + private boolean deconstructed = false; + + @Override + public void deconstruct() { + deconstructed = true; + } + } + + public static class TestDeconstructor implements ComponentDeconstructor { + @Override + public void deconstruct(List<Object> components, Collection<Bundle> bundles) { + components.forEach(component -> { + if (component instanceof DestructableComponent) { + DestructableComponent vespaComponent = (DestructableComponent) component; + vespaComponent.deconstruct(); + } + }); + if (! bundles.isEmpty()) throw new IllegalArgumentException("This test should not use bundles"); + } + } + + private static Container newContainer(DirConfigSource dirConfigSource, + ComponentDeconstructor deconstructor) { + return new Container(new CloudSubscriberFactory(dirConfigSource.configSource), dirConfigSource.configId(), deconstructor); + } + + private static Container newContainer(DirConfigSource dirConfigSource) { + return newContainer(dirConfigSource, new TestDeconstructor()); + } + + ComponentGraph getNewComponentGraph(Container container, ComponentGraph oldGraph) { + return container.getNewComponentGraph(oldGraph, Guice.createInjector(), true); + } + + ComponentGraph getNewComponentGraph(Container container) { + return container.getNewComponentGraph(new ComponentGraph(), Guice.createInjector(), true); + } + + private ComponentTakingConfig createComponentTakingConfig(ComponentGraph componentGraph) { + return componentGraph.getInstance(ComponentTakingConfig.class); + } + +} diff --git a/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java b/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java new file mode 100644 index 00000000000..2106a1f3671 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java @@ -0,0 +1,125 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di; + +import com.google.inject.Guice; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.config.FileReference; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.container.di.ContainerTest.ComponentTakingConfig; +import com.yahoo.container.di.componentgraph.core.ComponentGraph; +import com.yahoo.container.di.osgi.BundleClasses; +import org.junit.After; +import org.junit.Before; +import org.osgi.framework.Bundle; + +import java.util.Collection; +import java.util.Set; + +import static java.util.Collections.emptySet; + +/** + * @author Tony Vaagenes + * @author gjoranv + * @author ollivir + */ +public class ContainerTestBase { + + private ComponentGraph componentGraph; + protected DirConfigSource dirConfigSource = null; + + @Before + public void setup() { + dirConfigSource = new DirConfigSource("ContainerTest-"); + } + + @After + public void cleanup() { + dirConfigSource.cleanup(); + } + + @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 Set<Bundle> useApplicationBundles(Collection<FileReference> bundles) { + return emptySet(); + } + + @Override + public Bundle getBundle(ComponentSpecification spec) { + throw new UnsupportedOperationException("getBundle not supported."); + } + }); + componentGraph = container.getNewComponentGraph(componentGraph, Guice.createInjector(), true); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public <T> T getInstance(Class<T> componentClass) { + return componentGraph.getInstance(componentClass); + } + + protected void writeBootstrapConfigs(ComponentEntry... componentEntries) { + dirConfigSource.writeConfig("platform-bundles", ""); + dirConfigSource.writeConfig("application-bundles", ""); + StringBuilder components = new StringBuilder(); + for (int i = 0; i < componentEntries.length; i++) { + components.append(componentEntries[i].asConfig(i)); + components.append('\n'); + } + dirConfigSource.writeConfig("components", String.format("components[%s]\n%s", componentEntries.length, components)); + } + + protected void writeBootstrapConfigs(String componentId, Class<?> classId) { + writeBootstrapConfigs(new ComponentEntry(componentId, classId)); + } + + protected void writeBootstrapConfigs(String componentId) { + writeBootstrapConfigs(componentId, ComponentTakingConfig.class); + } + + protected void writeBootstrapConfigs() { + writeBootstrapConfigs(ComponentTakingConfig.class.getName(), ComponentTakingConfig.class); + } + + protected class ComponentEntry { + private final String componentId; + private final Class<?> classId; + + ComponentEntry(String componentId, Class<?> classId) { + this.componentId = componentId; + this.classId = classId; + } + + String asConfig(int position) { + return "<config>\n" + // + "components[" + position + "].id \"" + componentId + "\"\n" + // + "components[" + position + "].classId \"" + classId.getName() + "\"\n" + // + "components[" + position + "].configId \"" + dirConfigSource.configId() + "\"\n" + // + "</config>"; + } + } + +} diff --git a/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java b/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java new file mode 100644 index 00000000000..ec937a1a4ef --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java @@ -0,0 +1,69 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di; + +import com.yahoo.config.subscription.ConfigSource; +import com.yahoo.config.subscription.ConfigSourceSet; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Random; + +/** + * @author Tony Vaagenes + * @author gjoranv + * @author ollivir + */ +public class DirConfigSource { + private final TemporaryFolder tempFolder = createTemporaryFolder(); + public final ConfigSource configSource; + + public DirConfigSource(String testSourcePrefix) { + this.configSource = new ConfigSourceSet(testSourcePrefix + new Random().nextLong()); + } + + public void writeConfig(String name, String contents) { + File file = new File(tempFolder.getRoot(), name + ".cfg"); + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + printFile(file, contents + "\n"); + } + + public String configId() { + return "dir:" + tempFolder.getRoot().getPath(); + } + + public ConfigSource configSource() { + return configSource; + } + + public void cleanup() { + tempFolder.delete(); + } + + private static void printFile(File f, String content) { + try (OutputStream out = new FileOutputStream(f)) { + out.write(content.getBytes("UTF-8")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static TemporaryFolder createTemporaryFolder() { + TemporaryFolder folder = new TemporaryFolder(); + try { + folder.create(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return folder; + } +} diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java new file mode 100644 index 00000000000..70dc4c8665c --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java @@ -0,0 +1,750 @@ +// Copyright 2018 Yahoo Holdings. 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.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.yahoo.collections.Pair; +import com.yahoo.component.AbstractComponent; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.subscription.ConfigGetter; +import com.yahoo.config.test.Test2Config; +import com.yahoo.config.test.TestConfig; +import com.yahoo.container.di.Osgi; +import com.yahoo.container.di.componentgraph.Provider; +import com.yahoo.container.di.config.JerseyBundlesConfig; +import com.yahoo.container.di.config.JerseyInjectionConfig; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.vespa.config.ConfigKey; +import org.junit.Test; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +import static com.yahoo.container.di.componentgraph.core.ComponentGraph.isBindingAnnotation; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author gjoranv + * @author Tony Vaagenes + * @author ollivir + */ +public class ComponentGraphTest { + + public static class ConfigMap extends HashMap<ConfigKey<? extends ConfigInstance>, ConfigInstance> { + public ConfigMap() { + super(); + } + + public <T extends ConfigInstance> ConfigMap add(Class<T> clazz, String configId) { + ConfigKey<T> key = new ConfigKey<>(clazz, configId); + put(key, ConfigGetter.getConfig(key.getConfigClass(), key.getConfigId())); + return this; + } + + public static <T extends ConfigInstance> ConfigMap newMap(Class<T> clazz, String configId) { + ConfigMap ret = new ConfigMap(); + ret.add(clazz, configId); + return ret; + } + } + + @Test + public void component_taking_config_can_be_instantiated() { + ComponentGraph componentGraph = new ComponentGraph(); + String configId = "raw:stringVal \"test-value\""; + Node componentNode = mockComponentNode(ComponentTakingConfig.class, configId); + + componentGraph.add(componentNode); + componentGraph.complete(); + componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId)); + + ComponentTakingConfig instance = componentGraph.getInstance(ComponentTakingConfig.class); + assertNotNull(instance); + assertThat(instance.config.stringVal(), is("test-value")); + } + + @Test + public void all_created_components_are_returned_in_reverse_topological_order() { + for (int i = 0; i < 10; i++) { + Node innerComponent = mockComponentNode(SimpleComponent.class); + Node middleComponent = mockComponentNode(ComponentTakingComponent.class); + Node outerComponent = mockComponentNode(ComponentTakingComponentTakingComponent.class); + + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(innerComponent); + componentGraph.add(middleComponent); + componentGraph.add(outerComponent); + componentGraph.complete(); + + innerComponent.constructInstance(); + middleComponent.constructInstance(); + outerComponent.constructInstance(); + + assertEquals(List.of(outerComponent.constructedInstance().get(), middleComponent.constructedInstance().get(), innerComponent.constructedInstance().get()), + componentGraph.allConstructedComponentsAndProviders()); + } + } + + @Test + public void component_can_be_injected_into_another_component() { + Node injectedComponent = mockComponentNode(SimpleComponent.class); + Node targetComponent = mockComponentNode(ComponentTakingComponent.class); + targetComponent.inject(injectedComponent); + + Node destroyGlobalLookupComponent = mockComponentNode(SimpleComponent.class); + + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(injectedComponent); + componentGraph.add(targetComponent); + componentGraph.add(destroyGlobalLookupComponent); + componentGraph.complete(); + + ComponentTakingComponent instance = componentGraph.getInstance(ComponentTakingComponent.class); + assertNotNull(instance); + } + + @Test + public void interface_implementation_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(ComponentImpl.class)); + componentGraph.add(mockComponentNode(ComponentTakingInterface.class)); + componentGraph.complete(); + + ComponentTakingInterface instance = componentGraph.getInstance(ComponentTakingInterface.class); + assertTrue(instance.injected instanceof ComponentImpl); + } + + @Test + public void private_class_with_public_ctor_can_be_instantiated() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(PrivateClassComponent.class)); + componentGraph.complete(); + + PrivateClassComponent instance = componentGraph.getInstance(PrivateClassComponent.class); + assertNotNull(instance); + } + + @Test + public void all_components_of_a_type_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleDerivedComponent.class)); + componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class)); + componentGraph.complete(); + + ComponentTakingAllSimpleComponents instance = componentGraph.getInstance(ComponentTakingAllSimpleComponents.class); + assertThat(instance.simpleComponents.allComponents().size(), is(3)); + } + + @Test + public void empty_component_registry_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class)); + componentGraph.complete(); + + ComponentTakingAllSimpleComponents instance = componentGraph.getInstance(ComponentTakingAllSimpleComponents.class); + assertThat(instance.simpleComponents.allComponents().size(), is(0)); + } + + @Test + public void component_registry_with_wildcard_upper_bound_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleDerivedComponent.class)); + componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponentsUpperBound.class)); + componentGraph.complete(); + + ComponentTakingAllSimpleComponentsUpperBound instance = componentGraph + .getInstance(ComponentTakingAllSimpleComponentsUpperBound.class); + assertThat(instance.simpleComponents.allComponents().size(), is(2)); + } + + @Test(expected = RuntimeException.class) + public void require_exception_when_injecting_registry_with_unknown_type_variable() { + @SuppressWarnings("rawtypes") + Class<ComponentTakingAllComponentsWithTypeVariable> clazz = ComponentTakingAllComponentsWithTypeVariable.class; + + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleDerivedComponent.class)); + componentGraph.add(mockComponentNode(clazz)); + componentGraph.complete(); + + componentGraph.getInstance(clazz); + } + + @Test + public void components_are_shared() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.complete(); + + SimpleComponent instance1 = componentGraph.getInstance(SimpleComponent.class); + SimpleComponent instance2 = componentGraph.getInstance(SimpleComponent.class); + assertThat(instance1, sameInstance(instance2)); + } + + @Test + public void singleton_components_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + String configId = "raw:stringVal \"test-value\""; + + componentGraph.add(mockComponentNode(ComponentTakingComponent.class)); + componentGraph.add(mockComponentNode(ComponentTakingConfig.class, configId)); + componentGraph.add(mockComponentNode(SimpleComponent2.class)); + componentGraph.complete(); + componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId)); + + ComponentTakingComponent instance = componentGraph.getInstance(ComponentTakingComponent.class); + ComponentTakingConfig injected = (ComponentTakingConfig) instance.injectedComponent; + assertThat(injected.config.stringVal(), is("test-value")); + } + + @Test(expected = RuntimeException.class) + public void require_error_when_multiple_components_match_a_singleton_dependency() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleDerivedComponent.class)); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(ComponentTakingComponent.class)); + componentGraph.complete(); + } + + @Test + public void named_component_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleComponent.class, Names.named("named-test"))); + componentGraph.add(mockComponentNode(ComponentTakingNamedComponent.class)); + componentGraph.complete(); + } + + @Test + public void config_keys_can_be_retrieved() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(ComponentTakingConfig.class, "raw:stringVal \"component1\"")); + componentGraph.add(mockComponentNode(ComponentTakingConfig.class, "raw:stringVal \"component2\"")); + componentGraph.add(new ComponentRegistryNode(ComponentTakingConfig.class)); + componentGraph.complete(); + + Set<ConfigKey<? extends ConfigInstance>> configKeys = componentGraph.configKeys(); + assertThat(configKeys.size(), is(2)); + + configKeys.forEach(key -> { + assertThat(key.getConfigClass(), equalTo(TestConfig.class)); + assertThat(key.getConfigId(), containsString("component")); + }); + } + + @Test + public void providers_can_be_instantiated() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(ExecutorProvider.class)); + componentGraph.complete(); + + assertNotNull(componentGraph.getInstance(Executor.class)); + } + + @Test + public void providers_can_be_inherited() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(DerivedExecutorProvider.class)); + componentGraph.complete(); + + assertNotNull(componentGraph.getInstance(Executor.class)); + } + + @Test + public void providers_can_deliver_a_new_instance_for_each_component() { + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNode(NewIntProvider.class)); + componentGraph.complete(); + + Integer instance1 = componentGraph.getInstance(Integer.class); + Integer instance2 = componentGraph.getInstance(Integer.class); + assertEquals(1, instance1.intValue()); + assertEquals(2, instance2.intValue()); + } + + @Test + public void providers_can_be_injected_explicitly() { + ComponentGraph componentGraph = new ComponentGraph(); + + Node componentTakingExecutor = mockComponentNode(ComponentTakingExecutor.class); + Node executorProvider = mockComponentNode(ExecutorProvider.class); + componentTakingExecutor.inject(executorProvider); + + componentGraph.add(executorProvider); + componentGraph.add(mockComponentNode(ExecutorProvider.class)); + + componentGraph.add(componentTakingExecutor); + + componentGraph.complete(); + assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class)); + } + + @Test + public void global_providers_can_be_injected() { + ComponentGraph componentGraph = new ComponentGraph(); + + componentGraph.add(mockComponentNode(ComponentTakingExecutor.class)); + componentGraph.add(mockComponentNode(ExecutorProvider.class)); + componentGraph.add(mockComponentNode(FailOnGetIntProvider.class)); + componentGraph.complete(); + + assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class)); + } + + @Test(expected = RuntimeException.class) + public void throw_if_multiple_global_providers_exist() { + ComponentGraph componentGraph = new ComponentGraph(); + + componentGraph.add(mockComponentNode(ExecutorProvider.class)); + componentGraph.add(mockComponentNode(ExecutorProvider.class)); + componentGraph.add(mockComponentNode(ComponentTakingExecutor.class)); + componentGraph.complete(); + } + + @Test + public void provider_is_not_used_when_component_of_provided_class_exists() { + ComponentGraph componentGraph = new ComponentGraph(); + + componentGraph.add(mockComponentNode(SimpleComponent.class)); + componentGraph.add(mockComponentNode(SimpleComponentProviderThatThrows.class)); + componentGraph.add(mockComponentNode(ComponentTakingComponent.class)); + componentGraph.complete(); + + SimpleComponent injectedComponent = componentGraph.getInstance(ComponentTakingComponent.class).injectedComponent; + assertNotNull(injectedComponent); + } + + //TODO: move + @Test + public void check_if_annotation_is_a_binding_annotation() { + assertTrue(isBindingAnnotation(Names.named("name"))); + assertFalse(isBindingAnnotation(Named.class.getAnnotations()[0])); + } + + @Test + public void cycles_gives_exception() { + ComponentGraph componentGraph = new ComponentGraph(); + + Node node1 = mockComponentNode(ComponentCausingCycle.class); + Node node2 = mockComponentNode(ComponentCausingCycle.class); + + node1.inject(node2); + node2.inject(node1); + + componentGraph.add(node1); + componentGraph.add(node2); + + try { + componentGraph.complete(); + fail("Cycle exception expected."); + } catch (Throwable e) { + assertThat(e.getMessage(), containsString("cycle")); + assertThat(e.getMessage(), containsString("ComponentCausingCycle")); + } + } + + @Test(expected = IllegalArgumentException.class) + public void abstract_classes_are_rejected() { + new ComponentNode(ComponentId.fromString("Test"), "", AbstractClass.class); + } + + @Test + public void inject_constructor_is_preferred() { + assertThatComponentCanBeCreated(ComponentWithInjectConstructor.class); + } + + @Test + public void constructor_with_most_parameters_is_preferred() { + assertThatComponentCanBeCreated(ComponentWithMultipleConstructors.class); + } + + public void assertThatComponentCanBeCreated(Class<?> clazz) { + ComponentGraph componentGraph = new ComponentGraph(); + String configId = "raw:stringVal \"dummy\""; + + componentGraph.add(mockComponentNode(clazz, configId)); + componentGraph.complete(); + + componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId).add(Test2Config.class, configId)); + + assertNotNull(componentGraph.getInstance(clazz)); + } + + @Test + public void require_fallback_to_child_injector() { + ComponentGraph componentGraph = new ComponentGraph(); + + componentGraph.add(mockComponentNode(ComponentTakingExecutor.class)); + + componentGraph.complete(singletonExecutorInjector); + assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class)); + } + + @Test + public void child_injector_can_inject_multiple_instances_for_same_key() { + Pair<Integer, Pair<Executor, Executor>> graph = buildGraphWithChildInjector(Executors::newSingleThreadExecutor); + int graphSize = graph.getFirst(); + Executor executorA = graph.getSecond().getFirst(); + Executor executorB = graph.getSecond().getSecond(); + + assertThat(graphSize, is(4)); + assertThat(executorA, not(sameInstance(executorB))); + } + + @Test + public void components_injected_via_child_injector_can_be_shared() { + Executor commonExecutor = Executors.newSingleThreadExecutor(); + Pair<Integer, Pair<Executor, Executor>> graph = buildGraphWithChildInjector(() -> commonExecutor); + int graphSize = graph.getFirst(); + Executor executorA = graph.getSecond().getFirst(); + Executor executorB = graph.getSecond().getSecond(); + + assertThat(graphSize, is(3)); + assertThat(executorA, sameInstance(executorB)); + } + + private Pair<Integer, Pair<Executor, Executor>> buildGraphWithChildInjector(Supplier<Executor> executorProvider) { + Injector childInjector = Guice.createInjector(new AbstractModule() { + @Override + public void configure() { + bind(Executor.class).toProvider(executorProvider::get); + } + }); + + ComponentGraph componentGraph = new ComponentGraph(); + + Key<ComponentTakingExecutor> keyA = Key.get(ComponentTakingExecutor.class, Names.named("A")); + Key<ComponentTakingExecutor> keyB = Key.get(ComponentTakingExecutor.class, Names.named("B")); + + componentGraph.add(mockComponentNode(keyA)); + componentGraph.add(mockComponentNode(keyB)); + + componentGraph.complete(childInjector); + + return new Pair<>(componentGraph.size(), + new Pair<>(componentGraph.getInstance(keyA).executor, componentGraph.getInstance(keyB).executor)); + } + + @Test + public void providers_can_be_reused() { + + ComponentGraph oldGraph = createReusingGraph(); + Executor executor = oldGraph.getInstance(Executor.class); + + ComponentGraph newGraph = createReusingGraph(); + newGraph.reuseNodes(oldGraph); + + Executor newExecutor = newGraph.getInstance(Executor.class); + assertThat(executor, sameInstance(newExecutor)); + } + + private ComponentGraph createReusingGraph() { + ComponentGraph graph = new ComponentGraph(); + graph.add(mockComponentNodeWithId(ExecutorProvider.class, "dummyId")); + graph.complete(); + graph.setAvailableConfigs(Collections.emptyMap()); + return graph; + } + + @Test + public void component_id_can_be_injected() { + String componentId = "myId:1.2@namespace"; + + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(mockComponentNodeWithId(ComponentTakingComponentId.class, componentId)); + componentGraph.complete(); + + assertThat(componentGraph.getInstance(ComponentTakingComponentId.class).componentId, is(ComponentId.fromString(componentId))); + } + + @Test + public void rest_api_context_can_be_instantiated() { + String configId = "raw:\"\""; + + Class<RestApiContext> clazz = RestApiContext.class; + JerseyNode jerseyNode = new JerseyNode(uniqueComponentId(clazz.getName()), configId, clazz, new Osgi() { + }); + + ComponentGraph componentGraph = new ComponentGraph(); + componentGraph.add(jerseyNode); + componentGraph.complete(); + + componentGraph + .setAvailableConfigs(ConfigMap.newMap(JerseyBundlesConfig.class, configId).add(JerseyInjectionConfig.class, configId)); + + RestApiContext 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. + private static int counter = 0; + + private class PrivateClassComponent { + public PrivateClassComponent() { } + } + + public static class SimpleComponent extends AbstractComponent { + } + + public static class SimpleComponent2 extends AbstractComponent { + } + + public static class SimpleDerivedComponent extends SimpleComponent { + } + + public interface ComponentBase { } + public static class ComponentImpl implements ComponentBase { } + public static class ComponentTakingInterface { + ComponentBase injected; + public ComponentTakingInterface(ComponentBase componentBase) { + injected = componentBase; + } + } + + public static class ComponentTakingConfig extends SimpleComponent { + private final TestConfig config; + + public ComponentTakingConfig(TestConfig config) { + assertThat(config, notNullValue()); + this.config = config; + } + } + + public static class ComponentTakingComponent extends AbstractComponent { + private final SimpleComponent injectedComponent; + + public ComponentTakingComponent(SimpleComponent injectedComponent) { + assertThat(injectedComponent, notNullValue()); + this.injectedComponent = injectedComponent; + } + } + + public static class ComponentTakingComponentTakingComponent extends AbstractComponent { + private final ComponentTakingComponent injectedComponent; + + public ComponentTakingComponentTakingComponent(ComponentTakingComponent injectedComponent) { + assertThat(injectedComponent, notNullValue()); + this.injectedComponent = injectedComponent; + } + } + + @SuppressWarnings("unused") + public static class ComponentTakingConfigAndComponent extends AbstractComponent { + private final TestConfig config; + private final SimpleComponent simpleComponent; + + public ComponentTakingConfigAndComponent(TestConfig config, SimpleComponent injectedComponent) { + assertThat(config, notNullValue()); + assertThat(injectedComponent, notNullValue()); + this.config = config; + this.simpleComponent = injectedComponent; + } + } + + public static class ComponentTakingAllSimpleComponents extends AbstractComponent { + public final ComponentRegistry<SimpleComponent> simpleComponents; + + public ComponentTakingAllSimpleComponents(ComponentRegistry<SimpleComponent> simpleComponents) { + assertThat(simpleComponents, notNullValue()); + this.simpleComponents = simpleComponents; + } + } + + public static class ComponentTakingAllSimpleComponentsUpperBound extends AbstractComponent { + private final ComponentRegistry<? extends SimpleComponent> simpleComponents; + + public ComponentTakingAllSimpleComponentsUpperBound(ComponentRegistry<? extends SimpleComponent> simpleComponents) { + assertThat(simpleComponents, notNullValue()); + this.simpleComponents = simpleComponents; + } + } + + public static class ComponentTakingAllComponentsWithTypeVariable<COMPONENT extends AbstractComponent> extends AbstractComponent { + public ComponentTakingAllComponentsWithTypeVariable(ComponentRegistry<COMPONENT> simpleComponents) { + assertThat(simpleComponents, notNullValue()); + } + } + + public static class ComponentTakingNamedComponent extends AbstractComponent { + public ComponentTakingNamedComponent(@Named("named-test") SimpleComponent injectedComponent) { + assertThat(injectedComponent, notNullValue()); + } + } + + public static class ComponentCausingCycle extends AbstractComponent { + public ComponentCausingCycle(ComponentCausingCycle component) { + } + } + + public static class SimpleComponentProviderThatThrows implements Provider<SimpleComponent> { + public SimpleComponent get() { + throw new AssertionError("Should never be called."); + } + + public void deconstruct() { + } + } + + public static class ExecutorProvider implements Provider<Executor> { + private Executor executor = Executors.newSingleThreadExecutor(); + + public Executor get() { + return executor; + } + + public void deconstruct() { + /*TODO */ } + } + + public static class DerivedExecutorProvider extends ExecutorProvider { + } + + public static class FailOnGetIntProvider implements Provider<Integer> { + + public Integer get() { + fail("Should never be called."); + return null; + } + + public void deconstruct() { + } + + } + + public static class NewIntProvider implements Provider<Integer> { + int i = 0; + + public Integer get() { + return ++i; + } + + public void deconstruct() { + } + } + + public static class ComponentTakingExecutor extends AbstractComponent { + private final Executor executor; + + public ComponentTakingExecutor(Executor executor) { + assertThat(executor, notNullValue()); + this.executor = executor; + } + } + + public static class ComponentWithInjectConstructor { + + public ComponentWithInjectConstructor(TestConfig c, Test2Config c2) { + throw new RuntimeException("Should not be called"); + } + + @Inject + public ComponentWithInjectConstructor(Test2Config c) { + } + + } + + public static class ComponentWithMultipleConstructors { + + private ComponentWithMultipleConstructors(int dummy) { + } + + public ComponentWithMultipleConstructors() { + this(0); + throw new RuntimeException("Should not be called"); + } + + public ComponentWithMultipleConstructors(TestConfig c, Test2Config c2) { + this(0); + } + + public ComponentWithMultipleConstructors(Test2Config c) { + this(); + } + + } + + public static class ComponentTakingComponentId { + private final ComponentId componentId; + + public ComponentTakingComponentId(ComponentId componentId) { + this.componentId = componentId; + } + } + + public static ComponentId uniqueComponentId(String className) { + counter += 1; + return ComponentId.fromString(className + counter); + } + + public static Node mockComponentNode(Key<?> key) { + return mockComponentNode(key.getTypeLiteral().getRawType(), "", key.getAnnotation()); + } + + public static Node mockComponentNode(Class<?> clazz, String configId, Annotation key) { + return new ComponentNode(uniqueComponentId(clazz.getName()), configId, clazz, key); + } + + public static Node mockComponentNode(Class<?> clazz, String configId) { + return new ComponentNode(uniqueComponentId(clazz.getName()), configId, clazz, null); + } + + public static Node mockComponentNode(Class<?> clazz, Annotation key) { + return new ComponentNode(uniqueComponentId(clazz.getName()), "", clazz, key); + } + + public static Node mockComponentNode(Class<?> clazz) { + return new ComponentNode(uniqueComponentId(clazz.getName()), "", clazz, null); + } + + public static Node mockComponentNodeWithId(Class<?> clazz, String componentId, String configId /*= ""*/, Annotation key /*= null*/) { + return new ComponentNode(ComponentId.fromString(componentId), configId, clazz, key); + } + + public static Node mockComponentNodeWithId(Class<?> clazz, String componentId, String configId /*= ""*/) { + return new ComponentNode(ComponentId.fromString(componentId), configId, clazz, null); + } + + public static Node mockComponentNodeWithId(Class<?> clazz, String componentId) { + return new ComponentNode(ComponentId.fromString(componentId), "", clazz, null); + } + + public static Injector singletonExecutorInjector = Guice.createInjector(new AbstractModule() { + @Override + public void configure() { + bind(Executor.class).toInstance(Executors.newSingleThreadExecutor()); + } + }); + + public static abstract class AbstractClass { + } +} diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/FallbackToGuiceInjectorTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/FallbackToGuiceInjectorTest.java new file mode 100644 index 00000000000..7c517d67960 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/FallbackToGuiceInjectorTest.java @@ -0,0 +1,151 @@ +// Copyright 2017 Yahoo Holdings. 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.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 com.yahoo.component.ComponentId; +import com.yahoo.config.ConfigInstance; +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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.HashMap; +import java.util.Map; +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 Tony Vaagenes + * @author gjoranv + */ +@SuppressWarnings("unused") +public class FallbackToGuiceInjectorTest { + + private ComponentGraph componentGraph; + private Injector injector; + private Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs = + new HashMap<>(); + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Before + public void createGraph() { + injector = Guice.createInjector(); + componentGraph = new ComponentGraph(0); + } + + 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) { } + } + + @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 'com.yahoo.container.di.componentgraph.core.FallbackToGuiceInjectorTest$ComponentThatCannotBeConstructed'"); + complete(); + } + + 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); + } + + public void complete() { + componentGraph.complete(injector); + componentGraph.setAvailableConfigs(configs); + } + + public void setInjector(Injector injector) { + this.injector = injector; + } + + private Injector emptyGuiceInjector() { + return Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + } + }); + } +} diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java new file mode 100644 index 00000000000..f30f9260830 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java @@ -0,0 +1,68 @@ +// Copyright 2018 Yahoo Holdings. 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.container.bundle.MockBundle; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.di.osgi.OsgiUtil; +import org.junit.Test; +import org.osgi.framework.wiring.BundleWiring; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * @author gjoranv + * @author ollivir + */ + +public class JerseyNodeTest { + private MockBundle bundle; + private List<String> bundleClasses; + private final Map<String, String> resources; + + public JerseyNodeTest() { + resources = new HashMap<>(); + resources.put("com/foo", "com/foo/Foo.class"); + resources.put("com/bar", "com/bar/Bar.class"); + bundle = new MockBundle() { + @Override + public Collection<String> listResources(String path, String ignored, int options) { + if ((options & BundleWiring.LISTRESOURCES_RECURSE) != 0 && path.equals("/")) { + return resources.values(); + } else { + return Collections.singleton(resources.get(path)); + } + } + }; + bundleClasses = new ArrayList<>(resources.values()); + } + + @Test + public void all_bundle_entries_are_returned_when_no_packages_are_given() { + Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.emptySet()); + assertThat(entries, containsInAnyOrder(bundleClasses.toArray())); + } + + @Test + public void only_bundle_entries_from_the_given_packages_are_returned() { + Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.singleton("com.foo")); + assertThat(entries, contains(resources.get("com/foo"))); + } + + @Test + public void bundle_info_is_initialized() { + RestApiContext.BundleInfo bundleInfo = JerseyNode.createBundleInfo(bundle, Collections.emptyList()); + 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/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java new file mode 100644 index 00000000000..e61e90cd718 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java @@ -0,0 +1,254 @@ +// Copyright 2018 Yahoo Holdings. 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; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.subscription.ConfigGetter; +import com.yahoo.config.test.TestConfig; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingAllSimpleComponents; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingComponent; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingConfig; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingConfigAndComponent; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingExecutor; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ExecutorProvider; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent; +import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent2; +import com.yahoo.vespa.config.ConfigKey; +import org.junit.Test; + +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertThat; + +/** + * @author gjoranv + * @author Tony Vaagenes + * @author ollivir + */ +public class ReuseComponentsTest { + @Test + public void require_that_component_is_reused_when_componentNode_is_unmodified() { + reuseAndTest(SimpleComponent.class, SimpleComponent.class); + reuseAndTest(ExecutorProvider.class, Executor.class); + } + + private <T> void reuseAndTest(Class<?> classToRegister, Class<T> classToLookup) { + ComponentGraph graph = buildGraphAndSetNoConfigs(classToRegister); + T instance = getComponent(graph, classToLookup); + + ComponentGraph newGraph = buildGraphAndSetNoConfigs(classToRegister); + newGraph.reuseNodes(graph); + T instance2 = getComponent(newGraph, classToLookup); + + assertThat(instance2, sameInstance(instance)); + } + + @Test(expected = IllegalStateException.class) + public void require_that_component_is_not_reused_when_class_is_changed() { + ComponentGraph graph = buildGraphAndSetNoConfigs(SimpleComponent.class); + SimpleComponent instance = getComponent(graph, SimpleComponent.class); + + ComponentGraph newGraph = buildGraphAndSetNoConfigs(SimpleComponent2.class); + newGraph.reuseNodes(graph); + SimpleComponent2 instance2 = getComponent(newGraph, SimpleComponent2.class); + + assertThat(instance2.getId(), is(instance.getId())); + @SuppressWarnings("unused") + SimpleComponent throwsException = getComponent(newGraph, SimpleComponent.class); + } + + @Test + public void require_that_component_is_not_reused_when_config_is_changed() { + Class<ComponentTakingConfig> componentClass = ComponentTakingConfig.class; + + ComponentGraph graph = buildGraph(componentClass); + graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, "component"), + ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"oldConfig\""))); + ComponentTakingConfig instance = getComponent(graph, componentClass); + + ComponentGraph newGraph = buildGraph(componentClass); + newGraph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, "component"), + ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"newConfig\""))); + newGraph.reuseNodes(graph); + ComponentTakingConfig instance2 = getComponent(newGraph, componentClass); + + assertThat(instance2, not(sameInstance(instance))); + } + + @Test + public void require_that_component_is_not_reused_when_injected_component_is_changed() { + Function<String, ComponentGraph> buildGraph = config -> { + ComponentGraph graph = new ComponentGraph(); + + ComponentNode rootComponent = mockComponentNode(ComponentTakingComponent.class, "root_component"); + + String configId = "componentTakingConfigId"; + ComponentNode injectedComponent = mockComponentNode(ComponentTakingConfig.class, "injected_component", configId); + + rootComponent.inject(injectedComponent); + + graph.add(rootComponent); + graph.add(injectedComponent); + + graph.complete(); + graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, configId), + ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"" + config + "\""))); + + return graph; + }; + + ComponentGraph oldGraph = buildGraph.apply("oldGraph"); + ComponentTakingComponent oldInstance = getComponent(oldGraph, ComponentTakingComponent.class); + + ComponentGraph newGraph = buildGraph.apply("newGraph"); + newGraph.reuseNodes(oldGraph); + ComponentTakingComponent newInstance = getComponent(newGraph, ComponentTakingComponent.class); + + assertThat(newInstance, not(sameInstance(oldInstance))); + } + + @Test + public void require_that_component_is_not_reused_when_injected_component_registry_has_one_component_removed() { + Function<Boolean, ComponentGraph> buildGraph = useBothInjectedComponents -> { + ComponentGraph graph = new ComponentGraph(); + graph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class, "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(SimpleComponent.class, "injected_component2")); + graph.add(mockComponentNode(SimpleComponent.class, "injected_component1")); + } else { + graph.add(mockComponentNode(SimpleComponent.class, "injected_component1")); + } + + graph.complete(); + graph.setAvailableConfigs(Collections.emptyMap()); + return graph; + }; + + ComponentGraph oldGraph = buildGraph.apply(true); + ComponentRegistry<SimpleComponent> oldSimpleComponentRegistry = getComponent(oldGraph, ComponentTakingAllSimpleComponents.class).simpleComponents; + + ComponentGraph newGraph = buildGraph.apply(false); + newGraph.reuseNodes(oldGraph); + ComponentRegistry<SimpleComponent> newSimpleComponentRegistry = getComponent(newGraph, ComponentTakingAllSimpleComponents.class).simpleComponents; + + assertThat(newSimpleComponentRegistry, not(sameInstance(oldSimpleComponentRegistry))); + } + + @Test + public void require_that_injected_component_is_reused_even_when_dependent_component_is_changed() { + Function<String, ComponentGraph> buildGraph = config -> { + ComponentGraph graph = new ComponentGraph(); + + String configId = "componentTakingConfigAndComponent"; + ComponentNode rootComponent = mockComponentNode(ComponentTakingConfigAndComponent.class, "root_component", configId); + + ComponentNode injectedComponent = mockComponentNode(SimpleComponent.class, "injected_component"); + + rootComponent.inject(injectedComponent); + + graph.add(rootComponent); + graph.add(injectedComponent); + + graph.complete(); + graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, configId), + ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"" + config + "\""))); + + return graph; + }; + + ComponentGraph oldGraph = buildGraph.apply("oldGraph"); + SimpleComponent oldInjectedComponent = getComponent(oldGraph, SimpleComponent.class); + ComponentTakingConfigAndComponent oldDependentComponent = getComponent(oldGraph, ComponentTakingConfigAndComponent.class); + + ComponentGraph newGraph = buildGraph.apply("newGraph"); + newGraph.reuseNodes(oldGraph); + SimpleComponent newInjectedComponent = getComponent(newGraph, SimpleComponent.class); + ComponentTakingConfigAndComponent newDependentComponent = getComponent(newGraph, ComponentTakingConfigAndComponent.class); + + assertThat(newDependentComponent, not(sameInstance(oldDependentComponent))); + assertThat(newInjectedComponent, sameInstance(oldInjectedComponent)); + } + + @Test + public void require_that_node_depending_on_guice_node_is_reused() { + Supplier<ComponentGraph> makeGraph = () -> { + ComponentGraph graph = new ComponentGraph(); + graph.add(mockComponentNode(ComponentTakingExecutor.class, "dummyId")); + graph.complete(ComponentGraphTest.singletonExecutorInjector); + graph.setAvailableConfigs(Collections.emptyMap()); + return graph; + }; + + Function<ComponentGraph, ComponentTakingExecutor> componentRetriever = graph -> getComponent(graph, ComponentTakingExecutor.class); + + ComponentGraph oldGraph = makeGraph.get(); + componentRetriever.apply(oldGraph); // Ensure creation of GuiceNode + ComponentGraph newGraph = makeGraph.get(); + newGraph.reuseNodes(oldGraph); + assertThat(componentRetriever.apply(oldGraph), sameInstance(componentRetriever.apply(newGraph))); + } + + @Test + public void require_that_node_equals_only_checks_first_level_components_to_inject() { + Function<String, Node> createNodeWithInjectedNodeWithInjectedNode = indirectlyInjectedComponentId -> { + ComponentNode targetComponent = mockComponentNode(SimpleComponent.class, "target"); + ComponentNode directlyInjectedComponent = mockComponentNode(SimpleComponent.class, "directlyInjected"); + ComponentNode indirectlyInjectedComponent = mockComponentNode(SimpleComponent.class, indirectlyInjectedComponentId); + directlyInjectedComponent.inject(indirectlyInjectedComponent); + targetComponent.inject(directlyInjectedComponent); + + completeNode(targetComponent); + completeNode(directlyInjectedComponent); + completeNode(indirectlyInjectedComponent); + + return targetComponent; + }; + + Node targetNode1 = createNodeWithInjectedNodeWithInjectedNode.apply("indirectlyInjected_1"); + Node targetNode2 = createNodeWithInjectedNodeWithInjectedNode.apply("indirectlyInjected_2"); + assertThat(targetNode1, equalTo(targetNode2)); + } + + private void completeNode(ComponentNode node) { + node.setArguments(new Object[0]); + node.setAvailableConfigs(Collections.emptyMap()); + } + + private ComponentGraph buildGraph(Class<?> componentClass) { + String commonComponentId = "component"; + ComponentGraph g = new ComponentGraph(); + g.add(mockComponentNode(componentClass, commonComponentId, commonComponentId)); + g.complete(); + return g; + } + + private ComponentGraph buildGraphAndSetNoConfigs(Class<?> componentClass) { + ComponentGraph g = buildGraph(componentClass); + g.setAvailableConfigs(Collections.emptyMap()); + return g; + } + + private static ComponentNode mockComponentNode(Class<?> clazz, String componentId, String configId) { + return new ComponentNode(new ComponentId(componentId), configId, clazz); + } + + private static ComponentNode mockComponentNode(Class<?> clazz, String componentId) { + return mockComponentNode(clazz, componentId, ""); + } + + private static <T> T getComponent(ComponentGraph graph, Class<T> clazz) { + return graph.getInstance(clazz); + } +} diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java new file mode 100644 index 00000000000..174ca301c59 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/CycleFinderTest.java @@ -0,0 +1,85 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.di.componentgraph.cycle; + +import org.junit.Test; + +import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.A; +import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.B; +import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.C; +import static com.yahoo.container.di.componentgraph.cycle.CycleFinderTest.Vertices.D; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +/** + * @author gjoranv + */ +public class CycleFinderTest { + + enum Vertices {A, B, C, D} + + @Test + public void graph_without_cycles_returns_no_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + graph.edge(B, C); + graph.edge(A, C); + graph.edge(D, A); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), empty()); + } + + @Test + public void graph_with_cycle_returns_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + graph.edge(B, C); + graph.edge(C, A); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), contains(A, B, C, A)); + } + + @Test + public void graph_with_self_referencing_vertex_returns_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, A); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), contains(A, A)); + } + + @Test + public void leading_nodes_are_stripped_from_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + graph.edge(B, C); + graph.edge(C, B); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), contains(B, C, B)); + } + + @Test + public void findCycle_is_idempotent_with_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, A); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), contains(A, A)); + assertThat(cycleFinder.findCycle(), contains(A, A)); + } + + @Test + public void findCycle_is_idempotent_without_cycle() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + + var cycleFinder = new CycleFinder<>(graph); + assertThat(cycleFinder.findCycle(), empty()); + assertThat(cycleFinder.findCycle(), empty()); + } + +} diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java new file mode 100644 index 00000000000..069f72ad8e7 --- /dev/null +++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/cycle/GraphTest.java @@ -0,0 +1,67 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.di.componentgraph.cycle; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.A; +import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.B; +import static com.yahoo.container.di.componentgraph.cycle.GraphTest.Vertices.C; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +/** + * @author gjoranv + */ +public class GraphTest { + + enum Vertices {A, B, C} + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void vertices_and_edges_are_added_and_can_be_retrieved() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + graph.edge(B, C); + graph.edge(A, C); + + assertThat(graph.getVertices().size(), is(3)); + assertThat(graph.getAdjacent(A), containsInAnyOrder(B, C)); + assertThat(graph.getAdjacent(B), containsInAnyOrder(C)); + assertThat(graph.getAdjacent(C), empty()); + } + + @Test + public void null_vertices_are_not_allowed() { + var graph = new Graph<Vertices>(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Null vertices are not allowed"); + graph.edge(A, null); + } + + @Test + public void duplicate_edges_are_ignored() { + var graph = new Graph<Vertices>(); + graph.edge(A, B); + graph.edge(A, B); + + assertThat(graph.getAdjacent(A).size(), is(1)); + } + + @Test + public void self_edges_are_allowed() { + var graph = new Graph<Vertices>(); + graph.edge(A, A); + + assertThat(graph.getAdjacent(A), contains(A)); + } + +} diff --git a/container-di/src/test/vespa-configdef/config.di.int.def b/container-di/src/test/vespa-configdef/config.di.int.def new file mode 100644 index 00000000000..a34539c4a0f --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.di.int.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=config.di + +intVal int default=1 diff --git a/container-di/src/test/vespa-configdef/config.di.string.def b/container-di/src/test/vespa-configdef/config.di.string.def new file mode 100644 index 00000000000..396afe54f3f --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.di.string.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=config.di + +stringVal string default="_default_" diff --git a/container-di/src/test/vespa-configdef/config.test.bootstrap1.def b/container-di/src/test/vespa-configdef/config.test.bootstrap1.def new file mode 100644 index 00000000000..bdee16d99ea --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.bootstrap1.def @@ -0,0 +1,4 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=config.test + +dummy string default="" diff --git a/container-di/src/test/vespa-configdef/config.test.bootstrap2.def b/container-di/src/test/vespa-configdef/config.test.bootstrap2.def new file mode 100644 index 00000000000..b4fbffd8ae6 --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.bootstrap2.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=config.test + +dummy string default="" + diff --git a/container-di/src/test/vespa-configdef/config.test.components1.def b/container-di/src/test/vespa-configdef/config.test.components1.def new file mode 100644 index 00000000000..bdee16d99ea --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.components1.def @@ -0,0 +1,4 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=config.test + +dummy string default="" diff --git a/container-di/src/test/vespa-configdef/config.test.test.def b/container-di/src/test/vespa-configdef/config.test.test.def new file mode 100644 index 00000000000..d3e0ed17748 --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.test.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=config.test + +stringVal string default="default" diff --git a/container-di/src/test/vespa-configdef/config.test.test2.def b/container-di/src/test/vespa-configdef/config.test.test2.def new file mode 100644 index 00000000000..d3e0ed17748 --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.test2.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=config.test + +stringVal string default="default" diff --git a/container-di/src/test/vespa-configdef/config.test.thread-pool.def b/container-di/src/test/vespa-configdef/config.test.thread-pool.def new file mode 100644 index 00000000000..9e6b6694e84 --- /dev/null +++ b/container-di/src/test/vespa-configdef/config.test.thread-pool.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +namespace=config.test + +numThreads int |