package com.google.gwt.inject.rebind.resolution;
import static com.google.gwt.inject.rebind.resolution.TestUtils.bar;
import static com.google.gwt.inject.rebind.resolution.TestUtils.baz;
import static com.google.gwt.inject.rebind.resolution.TestUtils.foo;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.inject.rebind.ErrorManager;
import com.google.gwt.inject.rebind.GinjectorBindings;
import com.google.gwt.inject.rebind.binding.Binding;
import com.google.gwt.inject.rebind.binding.Context;
import com.google.gwt.inject.rebind.binding.Dependency;
import com.google.gwt.inject.rebind.binding.ExposedChildBinding;
import com.google.gwt.inject.rebind.resolution.DependencyExplorer.DependencyExplorerOutput;
import com.google.gwt.inject.rebind.util.Preconditions;
import com.google.inject.Key;
import junit.framework.TestCase;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Tests for {@link BindingPositioner}.
*/
public class BindingPositionerTest extends TestCase {
private static final String SOURCE = "dummy";
private IMocksControl control;
private ErrorManager errorManager;
private GinjectorBindings root;
private GinjectorBindings child;
private GinjectorBindings grandchild;
private GinjectorBindings othergrandchild; // Only used as the source of an ExposedChildBinding.
private final TreeLogger treeLogger = TreeLogger.NULL;
@Override
protected void setUp() throws Exception {
control = EasyMock.createControl();
errorManager = control.createMock("errorManager", ErrorManager.class);
root = control.createMock("root", GinjectorBindings.class);
child = control.createMock("child", GinjectorBindings.class);
grandchild = control.createMock("grandchild", GinjectorBindings.class);
othergrandchild = control.createMock("other", GinjectorBindings.class);
expect(grandchild.getParent()).andStubReturn(child);
expect(child.getParent()).andStubReturn(root);
expect(root.getParent()).andStubReturn(null);
expect(root.isBoundLocallyInChild(isA(Key.class))).andStubReturn(false);
expect(child.isBoundLocallyInChild(isA(Key.class))).andStubReturn(false);
expect(grandchild.isBoundLocallyInChild(isA(Key.class))).andStubReturn(false);
expect(root.getBinding(isA(Key.class))).andStubReturn(null);
expect(child.getBinding(isA(Key.class))).andStubReturn(null);
expect(grandchild.getBinding(isA(Key.class))).andStubReturn(null);
expect(root.isPinned(isA(Key.class))).andStubReturn(false);
expect(child.isPinned(isA(Key.class))).andStubReturn(false);
expect(grandchild.isPinned(isA(Key.class))).andStubReturn(false);
}
public void testNoDependencies() throws Exception {
new PositionerExpectationsBuilder(child).test();
}
public void testAlreadyPositioned() throws Exception {
// Verifies that a single, already positioned node stays where it is
new PositionerExpectationsBuilder(child)
.keysBoundAt(child, foo())
.test();
}
private PositionerExpectationsBuilder testTree() {
return new PositionerExpectationsBuilder(grandchild)
.addEdge(new Dependency(foo(), bar(), SOURCE))
.addEdge(new Dependency(foo(), baz(), SOURCE));
}
public void testPositionTree() throws Exception {
// Foo (which depends on Bar and Baz) ends up "no-higher than the lowest" of bar and baz.
testTree()
.implicitlyBoundAt(child, foo())
.keysBoundAt(child, bar())
.keysBoundAt(root, baz())
.test();
}
public void testPositionTree_BoundInChild() throws Exception {
// Bar can't be placed at root (already bound in child), so it and foo get placed in child.
expect(root.isBoundLocallyInChild(bar())).andReturn(true).anyTimes(); // bar() must be in child
testTree()
.implicitlyBoundAt(child, foo(), bar())
.implicitlyBoundAt(root, baz())
.test();
}
private PositionerExpectationsBuilder testChain() {
return new PositionerExpectationsBuilder(grandchild)
.addEdge(new Dependency(foo(), bar(), SOURCE))
.addEdge(new Dependency(bar(), baz(), SOURCE));
}
public void testPositionChain() throws Exception {
testChain()
.implicitlyBoundAt(root, foo(), bar(), baz())
.test();
}
public void testPositionChain_FooBoundInSibling() throws Exception {
expect(root.isBoundLocallyInChild(foo())).andReturn(true).anyTimes();
expect(child.isBoundLocallyInChild(foo())).andReturn(true).anyTimes();
testChain()
.implicitlyBoundAt(root, bar(), baz())
.implicitlyBoundAt(grandchild, foo())
.test();
}
public void testPositionChain_BazBoundInRoot() throws Exception {
testChain()
.implicitlyBoundAt(child, foo(), bar())
.keysBoundAt(child, baz())
.test();
}
private PositionerExpectationsBuilder testCycle() {
return new PositionerExpectationsBuilder(grandchild)
.addEdge(new Dependency(foo(), bar(), SOURCE))
.addEdge(new Dependency(bar(), baz(), SOURCE))
.addEdge(new Dependency(baz(), foo(), SOURCE));
}
public void testPositionCycle() throws Exception {
testCycle()
.implicitlyBoundAt(root, foo(), bar(), baz())
.test();
}
public void testPositionCycle_BarBoundInSibling() throws Exception {
// Cycle through foo -> bar -> baz, and bar must be placed in child, so everything is placed
// in child.
expect(root.isBoundLocallyInChild(bar())).andReturn(true).anyTimes(); // bar() must be in child
testCycle()
.implicitlyBoundAt(child, bar(), baz(), foo())
.test();
}
public void testPositionCycle_BazBoundInChild() throws Exception {
testCycle()
.keysBoundAt(child, baz())
.implicitlyBoundAt(child, bar(), foo())
.test();
}
public void testPositionCycle_BarAndBazBoundInSibling() throws Exception {
expect(root.isBoundLocallyInChild(bar())).andReturn(true).anyTimes();
expect(root.isBoundLocallyInChild(baz())).andReturn(true).anyTimes();
expect(child.isBoundLocallyInChild(baz())).andReturn(true).anyTimes();
testCycle()
.implicitlyBoundAt(grandchild, foo(), bar(), baz())
.test();
}
public void testPositionCycle_OutsideDep() throws Exception {
testCycle()
.addEdge(new Dependency(bar(), Key.get(A.class), SOURCE))
.keysBoundAt(child, Key.get(A.class))
.implicitlyBoundAt(child, foo(), bar(), baz())
.test();
}
public void testPositionPinned_noBindingInParent() throws Exception {
// Bar is bound (and pinned) at grandchild, but because it is not exposed to
// the root, foo should be created in grandchild.
testChain()
.addEdge(new Dependency(foo(), bar(), SOURCE))
.pinnedAt(grandchild, bar())
.implicitlyBoundAt(grandchild, bar())
.implicitlyBoundAt(grandchild, foo())
.test();
}
public void testPositionPinned_exposedBindingInParent() throws Exception {
// Bar is bound (and pinned) at grandchild, but because it is exposed to the
// child, foo should be created up there.
testChain()
.addEdge(new Dependency(foo(), bar(), SOURCE))
.pinnedAt(grandchild, bar())
.exposed(grandchild, child, bar())
.implicitlyBoundAt(grandchild, bar())
.implicitlyBoundAt(child, foo())
.test();
}
public void testPositionPinned_exposedBindingInParent_fromOtherChild() throws Exception {
// Bar is bound (and pinned) at grandchild. It is exposed to the parent
// from a different grandchild, and the positioner should throw an exception
// in this case.
//
// (this should be an error in other parts of the code, but check that this
// module behaves in a well-defined way)
testChain()
.addEdge(new Dependency(foo(), bar(), SOURCE))
.pinnedAt(grandchild, bar())
.exposed(othergrandchild, child, bar())
.shouldThrow(Exception.class)
.implicitlyBoundAt(grandchild, bar())
.implicitlyBoundAt(grandchild, foo())
.test();
}
public void testPositionPinned_bindingInParent_notExposedBinding() throws Exception {
// Bar is bound (and pinned) at grandchild. It is available in the parent
// with a different binding, and the positioner should throw an exception in
// this case.
//
// (this should be an error in other parts of the code, but check that this
// module behaves in a well-defined way)
testChain()
.addEdge(new Dependency(foo(), bar(), SOURCE))
.pinnedAt(grandchild, bar())
.notExposedBinding(child, bar())
.shouldThrow(Exception.class)
.implicitlyBoundAt(grandchild, bar())
.implicitlyBoundAt(grandchild, foo())
.test();
}
public void testPositionPinned_exposedBindingInParentAndGrandparent() throws Exception {
// Bar is bound (and pinned) at grandchild, but because it is exposed to the
// child and the root, foo should be created up there.
testChain()
.addEdge(new Dependency(foo(), bar(), SOURCE))
.pinnedAt(grandchild, bar())
.exposed(grandchild, child, bar())
.exposed(child, root, bar())
.implicitlyBoundAt(grandchild, bar())
.implicitlyBoundAt(root, foo())
.test();
}
private static class A {}
private static class B {}
private static class C {}
private static class D {}
public void testTwoCycles() throws Exception {
testCycle()
.addEdge(new Dependency(bar(), Key.get(A.class), SOURCE))
.addEdge(new Dependency(Key.get(A.class), Key.get(B.class), SOURCE))
.addEdge(new Dependency(Key.get(B.class), Key.get(C.class), SOURCE))
.addEdge(new Dependency(Key.get(B.class), Key.get(D.class), SOURCE))
.addEdge(new Dependency(Key.get(C.class), Key.get(A.class), SOURCE))
.keysBoundAt(child, Key.get(D.class))
.implicitlyBoundAt(child, foo(), bar(), baz(),
Key.get(A.class), Key.get(B.class), Key.get(C.class))
.test();
}
/**
* Builder for constructing the expectations used above in a more readable manner.
*/
private class PositionerExpectationsBuilder {
private final DependencyGraph.Builder graphBuilder;
private final Map<Key<?>, GinjectorBindings> implicitlyBoundKeys =
new HashMap<Key<?>, GinjectorBindings>();
private final Map<Key<?>, GinjectorBindings> preExistingLocations =
new HashMap<Key<?>, GinjectorBindings>();
private final Map<GinjectorBindings, Map<Key<?>, GinjectorBindings>> exposedTo =
new HashMap<GinjectorBindings, Map<Key<?>, GinjectorBindings>>();
private final Map<GinjectorBindings, Set<Key<?>>> notExposedBinding =
new HashMap<GinjectorBindings, Set<Key<?>>>();
private Class<?> thrownType = null;
public PositionerExpectationsBuilder(GinjectorBindings origin) {
graphBuilder = new DependencyGraph.Builder(origin);
}
public PositionerExpectationsBuilder addEdge(Dependency dependency) {
graphBuilder.addEdge(dependency);
return this;
}
public PositionerExpectationsBuilder implicitlyBoundAt(
GinjectorBindings expected, Key<?>... keys) {
for (Key<?> key : keys) {
Preconditions.checkState(!preExistingLocations.containsKey(key),
"Key %s cannot be implicitly bound -- already bound in ginjector %s!", key,
preExistingLocations.get(key));
implicitlyBoundKeys.put(key, expected);
}
return this;
}
public PositionerExpectationsBuilder exposed(
GinjectorBindings child, GinjectorBindings parent, Key<?>... keys) {
for (Key<?> key : keys) {
Preconditions.checkState(
!(notExposedBinding.containsKey(parent) && notExposedBinding.get(parent).contains(key)),
"Key %s cannot be exposed: it has a not-exposed binding in %s!", key, parent);
}
Map<Key<?>, GinjectorBindings> keyToChildMap = exposedTo.get(parent);
if (keyToChildMap == null) {
keyToChildMap = new HashMap<Key<?>, GinjectorBindings>();
exposedTo.put(parent, keyToChildMap);
}
for (Key<?> key : keys) {
keyToChildMap.put(key, child);
}
return this;
}
public PositionerExpectationsBuilder notExposedBinding(
GinjectorBindings parent, Key<?>... keys) {
Set<Key<?>> keySet = notExposedBinding.get(parent);
if (keySet == null) {
keySet = new HashSet<Key<?>>();
notExposedBinding.put(parent, keySet);
}
for (Key<?> key : keys) {
Preconditions.checkState(
!(exposedTo.containsKey(parent) && exposedTo.get(parent).containsKey(key)),
"Key %s cannot have a not-exposed binding: it already is exposed to %s!", key, parent);
keySet.add(key);
}
return this;
}
public PositionerExpectationsBuilder keysBoundAt(GinjectorBindings ginjector, Key<?>... keys) {
for (Key<?> key : keys) {
Preconditions.checkState(!implicitlyBoundKeys.containsKey(key),
"Key %s cannot be bound at %s -- already in implicitly bound set!", key, ginjector);
preExistingLocations.put(key, ginjector);
}
return this;
}
public PositionerExpectationsBuilder pinnedAt(GinjectorBindings ginjector, Key<?>... keys) {
for (Key<?> key : keys) {
expect(ginjector.isPinned(key)).andReturn(true).anyTimes();
}
return this;
}
public PositionerExpectationsBuilder shouldThrow(Class<?> thrownType) {
this.thrownType = thrownType;
return this;
}
public void test() {
DependencyExplorerOutput output = control.createMock(DependencyExplorerOutput.class);
expect(output.getGraph()).andStubReturn(graphBuilder.build());
expect(output.getImplicitlyBoundKeys()).andStubReturn(implicitlyBoundKeys.keySet());
expect(output.getPreExistingLocations()).andStubReturn(preExistingLocations);
expectExposedBindingsExist();
expectNotExposedBindingsExist();
control.replay();
BindingPositioner positioner = new BindingPositioner(treeLogger);
RuntimeException actuallyThrownException = null;
try {
positioner.position(output);
} catch (RuntimeException exception) {
actuallyThrownException = exception;
}
// If we expected an exception, make sure it happened. Otherwise, verify the results.
if (thrownType != null) {
// Distinguish the "wrong type" vs "nothing at all" cases to get better
// error messages.
if (actuallyThrownException == null) {
fail("Expected " + thrownType);
} else if (!thrownType.isInstance(actuallyThrownException)) {
// The positioner failed in an unexpected way -- let the user see the
// stack trace.
throw new RuntimeException("Wrong exception type (expected " + thrownType + ")",
actuallyThrownException);
}
} else {
if (actuallyThrownException != null) {
throw new RuntimeException("Unexpected exception", actuallyThrownException);
}
// Check that already positioned things didn't move
for (Map.Entry<Key<?>, GinjectorBindings> entry : preExistingLocations.entrySet()) {
assertSame(String.format("Expected already-bound %s to remain in location %s, but was %s",
entry.getKey(), entry.getValue(), positioner.getInstallPosition(entry.getKey())),
entry.getValue(), positioner.getInstallPosition(entry.getKey()));
}
// Check that implicitly bound keys ended up where we expect
for (Map.Entry<Key<?>, GinjectorBindings> entry : implicitlyBoundKeys.entrySet()) {
assertSame(String.format("Expected %s to be placed at %s, but was %s",
entry.getKey(), entry.getValue(), positioner.getInstallPosition(entry.getKey())),
entry.getValue(), positioner.getInstallPosition(entry.getKey()));
}
}
control.verify();
}
/**
* For each binding exposed from a child, expect it to be represented
* in the parent by an {@link ExposedChildBinding}.
*/
private void expectExposedBindingsExist() {
for (Map.Entry<GinjectorBindings, Map<Key<?>, GinjectorBindings>> entry :
exposedTo.entrySet()) {
GinjectorBindings parent = entry.getKey();
Map<Key<?>, GinjectorBindings> keyToChildMap = entry.getValue();
for (Map.Entry<Key<?>, GinjectorBindings> keyAndChild : keyToChildMap.entrySet()) {
Key<?> key = keyAndChild.getKey();
GinjectorBindings child = keyAndChild.getValue();
expect(parent.getBinding(key))
.andReturn(new ExposedChildBinding(errorManager, Key.get(Long.class), child,
Context.forText("")))
.anyTimes();
}
}
}
/**
* For each non-exposed binding, expect it to return an arbitrary binding
* implementation that is never accessed.
*/
private void expectNotExposedBindingsExist() {
for (Map.Entry<GinjectorBindings, Set<Key<?>>> entry : notExposedBinding.entrySet()) {
GinjectorBindings bindings = entry.getKey();
Set<Key<?>> keys = entry.getValue();
for(Key<?> key : keys) {
expect(bindings.getBinding(key))
.andReturn(createMock(Binding.class))
.anyTimes();
}
}
}
}
}