1/**
2 * Copyright (C) 2008 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.inject;
18
19import static com.google.inject.Asserts.assertContains;
20import static com.google.inject.name.Names.named;
21import static java.lang.annotation.ElementType.METHOD;
22import static java.lang.annotation.ElementType.TYPE;
23import static java.lang.annotation.RetentionPolicy.RUNTIME;
24
25import com.google.common.collect.ImmutableList;
26import com.google.common.collect.Lists;
27import com.google.inject.binder.AnnotatedBindingBuilder;
28import com.google.inject.binder.ScopedBindingBuilder;
29import com.google.inject.name.Named;
30import com.google.inject.util.Providers;
31
32import junit.framework.Test;
33import junit.framework.TestCase;
34import junit.framework.TestSuite;
35
36import java.lang.annotation.Retention;
37import java.lang.annotation.Target;
38import java.util.Collections;
39import java.util.List;
40import java.util.concurrent.atomic.AtomicInteger;
41
42/**
43 * @author jessewilson@google.com (Jesse Wilson)
44 */
45public class BinderTestSuite extends TestCase {
46
47  public static Test suite() {
48    TestSuite suite = new TestSuite();
49
50    new Builder()
51        .name("bind A")
52        .module(new AbstractModule() {
53          protected void configure() {
54            bind(A.class);
55          }
56        })
57        .creationException("No implementation for %s was bound", A.class.getName())
58        .addToSuite(suite);
59
60    new Builder()
61        .name("bind PlainA named apple")
62        .module(new AbstractModule() {
63          protected void configure() {
64            bind(PlainA.class).annotatedWith(named("apple"));
65          }
66        })
67        .creationException("No implementation for %s annotated with %s was bound",
68            PlainA.class.getName(), named("apple"))
69        .addToSuite(suite);
70
71    new Builder()
72        .name("bind A to new PlainA(1)")
73        .module(new AbstractModule() {
74          protected void configure() {
75            bind(A.class).toInstance(new PlainA(1));
76          }
77        })
78        .creationTime(CreationTime.NONE)
79        .expectedValues(new PlainA(1), new PlainA(1), new PlainA(1))
80        .addToSuite(suite);
81
82    new Builder()
83        .name("no binding, AWithProvidedBy")
84        .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class)
85        .addToSuite(suite);
86
87    new Builder()
88        .name("no binding, AWithImplementedBy")
89        .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class)
90        .addToSuite(suite);
91
92    new Builder()
93        .name("no binding, ScopedA")
94        .key(Key.get(ScopedA.class), InjectsScopedA.class)
95        .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202))
96        .addToSuite(suite);
97
98    new Builder()
99        .name("no binding, AWithProvidedBy named apple")
100        .key(Key.get(AWithProvidedBy.class, named("apple")),
101            InjectsAWithProvidedByNamedApple.class)
102        .configurationException("No implementation for %s annotated with %s was bound",
103            AWithProvidedBy.class.getName(), named("apple"))
104        .addToSuite(suite);
105
106    new Builder()
107        .name("no binding, AWithImplementedBy named apple")
108        .key(Key.get(AWithImplementedBy.class, named("apple")),
109            InjectsAWithImplementedByNamedApple.class)
110        .configurationException("No implementation for %s annotated with %s was bound",
111            AWithImplementedBy.class.getName(), named("apple"))
112        .addToSuite(suite);
113
114    new Builder()
115        .name("no binding, ScopedA named apple")
116        .key(Key.get(ScopedA.class, named("apple")), InjectsScopedANamedApple.class)
117        .configurationException("No implementation for %s annotated with %s was bound",
118            ScopedA.class.getName(), named("apple"))
119        .addToSuite(suite);
120
121    for (final Scoper scoper : Scoper.values()) {
122      new Builder()
123          .name("bind PlainA")
124          .key(Key.get(PlainA.class), InjectsPlainA.class)
125          .module(new AbstractModule() {
126            protected void configure() {
127              AnnotatedBindingBuilder<PlainA> abb = bind(PlainA.class);
128              scoper.configure(abb);
129            }
130          })
131          .scoper(scoper)
132          .addToSuite(suite);
133
134      new Builder()
135          .name("bind A to PlainA")
136          .module(new AbstractModule() {
137            protected void configure() {
138              ScopedBindingBuilder sbb = bind(A.class).to(PlainA.class);
139              scoper.configure(sbb);
140            }
141          })
142          .scoper(scoper)
143          .addToSuite(suite);
144
145      new Builder()
146          .name("bind A to PlainAProvider.class")
147          .module(new AbstractModule() {
148            protected void configure() {
149              ScopedBindingBuilder sbb = bind(A.class).toProvider(PlainAProvider.class);
150              scoper.configure(sbb);
151            }
152          })
153          .scoper(scoper)
154          .addToSuite(suite);
155
156      new Builder()
157          .name("bind A to new PlainAProvider()")
158          .module(new AbstractModule() {
159            protected void configure() {
160              ScopedBindingBuilder sbb = bind(A.class).toProvider(new PlainAProvider());
161              scoper.configure(sbb);
162            }
163          })
164          .scoper(scoper)
165          .addToSuite(suite);
166
167      new Builder()
168          .name("bind AWithProvidedBy")
169          .key(Key.get(AWithProvidedBy.class), InjectsAWithProvidedBy.class)
170          .module(new AbstractModule() {
171            protected void configure() {
172              ScopedBindingBuilder sbb = bind(AWithProvidedBy.class);
173              scoper.configure(sbb);
174            }
175          })
176          .scoper(scoper)
177          .addToSuite(suite);
178
179      new Builder()
180          .name("bind AWithImplementedBy")
181          .key(Key.get(AWithImplementedBy.class), InjectsAWithImplementedBy.class)
182          .module(new AbstractModule() {
183            protected void configure() {
184              ScopedBindingBuilder sbb = bind(AWithImplementedBy.class);
185              scoper.configure(sbb);
186            }
187          })
188          .scoper(scoper)
189          .addToSuite(suite);
190
191      new Builder()
192          .name("bind ScopedA")
193          .key(Key.get(ScopedA.class), InjectsScopedA.class)
194          .module(new AbstractModule() {
195            protected void configure() {
196              ScopedBindingBuilder sbb = bind(ScopedA.class);
197              scoper.configure(sbb);
198            }
199          })
200          .expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202))
201          .scoper(scoper)
202          .addToSuite(suite);
203
204
205      new Builder()
206          .name("bind AWithProvidedBy named apple")
207          .module(new AbstractModule() {
208            protected void configure() {
209              scoper.configure(bind(AWithProvidedBy.class).annotatedWith(named("apple")));
210            }
211          })
212          .creationException("No implementation for %s annotated with %s was bound",
213              AWithProvidedBy.class.getName(), named("apple"))
214          .addToSuite(suite);
215
216      new Builder()
217          .name("bind AWithImplementedBy named apple")
218          .module(new AbstractModule() {
219            protected void configure() {
220              scoper.configure(bind(AWithImplementedBy.class).annotatedWith(named("apple")));
221            }
222          })
223          .creationException("No implementation for %s annotated with %s was bound",
224              AWithImplementedBy.class.getName(), named("apple"))
225          .addToSuite(suite);
226
227      new Builder()
228          .name("bind ScopedA named apple")
229          .module(new AbstractModule() {
230            protected void configure() {
231              scoper.configure(bind(ScopedA.class).annotatedWith(named("apple")));
232            }
233          })
234          .creationException("No implementation for %s annotated with %s was bound",
235              ScopedA.class.getName(), named("apple"))
236          .addToSuite(suite);
237
238    }
239
240    return suite;
241  }
242
243  enum Scoper {
244    UNSCOPED {
245      void configure(ScopedBindingBuilder sbb) {}
246      void apply(Builder builder) {}
247    },
248
249    EAGER_SINGLETON {
250      void configure(ScopedBindingBuilder sbb) {
251        sbb.asEagerSingleton();
252      }
253      void apply(Builder builder) {
254        builder.expectedValues(new PlainA(101), new PlainA(101), new PlainA(101));
255        builder.creationTime(CreationTime.EAGER);
256      }
257    },
258
259    SCOPES_SINGLETON {
260      void configure(ScopedBindingBuilder sbb) {
261        sbb.in(Scopes.SINGLETON);
262      }
263      void apply(Builder builder) {
264        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201));
265      }
266    },
267
268    SINGLETON_DOT_CLASS {
269      void configure(ScopedBindingBuilder sbb) {
270        sbb.in(Singleton.class);
271      }
272      void apply(Builder builder) {
273        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(201));
274      }
275    },
276
277    TWO_AT_A_TIME_SCOPED_DOT_CLASS {
278      void configure(ScopedBindingBuilder sbb) {
279        sbb.in(TwoAtATimeScoped.class);
280      }
281      void apply(Builder builder) {
282        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202));
283      }
284    },
285
286    TWO_AT_A_TIME_SCOPE {
287      void configure(ScopedBindingBuilder sbb) {
288        sbb.in(new TwoAtATimeScope());
289      }
290      void apply(Builder builder) {
291        builder.expectedValues(new PlainA(201), new PlainA(201), new PlainA(202), new PlainA(202));
292      }
293    };
294
295    abstract void configure(ScopedBindingBuilder sbb);
296    abstract void apply(Builder builder);
297  }
298
299  /** When Guice creates a value, directly or via a provider */
300  enum CreationTime {
301    NONE, EAGER, LAZY
302  }
303
304  public static class Builder {
305    private String name = "test";
306    private Key<?> key = Key.get(A.class);
307    private Class<? extends Injectable> injectsKey = InjectsA.class;
308    private List<Module> modules = Lists.<Module>newArrayList(new AbstractModule() {
309      protected void configure() {
310        bindScope(TwoAtATimeScoped.class, new TwoAtATimeScope());
311      }
312    });
313    private List<Object> expectedValues = Lists.<Object>newArrayList(
314        new PlainA(201), new PlainA(202), new PlainA(203));
315    private CreationTime creationTime = CreationTime.LAZY;
316    private String creationException;
317    private String configurationException;
318
319    public Builder module(Module module) {
320      this.modules.add(module);
321      return this;
322    }
323
324    public Builder creationTime(CreationTime creationTime) {
325      this.creationTime = creationTime;
326      return this;
327    }
328
329    public Builder name(String name) {
330      this.name = name;
331      return this;
332    }
333
334    public Builder key(Key<?> key, Class<? extends Injectable> injectsKey) {
335      this.key = key;
336      this.injectsKey = injectsKey;
337      return this;
338    }
339
340    private Builder creationException(String message, Object... args) {
341      this.creationException = String.format(message, args);
342      return this;
343    }
344
345    private Builder configurationException(String message, Object... args) {
346      configurationException = String.format(message, args);
347      return this;
348    }
349
350    private Builder scoper(Scoper scoper) {
351      name(name + " in " + scoper);
352      scoper.apply(this);
353      return this;
354    }
355
356    private <T> Builder expectedValues(T... values) {
357      this.expectedValues.clear();
358      Collections.addAll(this.expectedValues, values);
359      return this;
360    }
361
362    public void addToSuite(TestSuite suite) {
363      if (creationException != null) {
364        suite.addTest(new CreationExceptionTest(this));
365
366      } else if (configurationException != null) {
367        suite.addTest(new ConfigurationExceptionTest(this));
368
369      } else {
370        suite.addTest(new SuccessTest(this));
371        if (creationTime != CreationTime.NONE) {
372          suite.addTest(new UserExceptionsTest(this));
373        }
374      }
375    }
376  }
377
378  public static class SuccessTest extends TestCase {
379    final String name;
380    final Key<?> key;
381    final Class<? extends Injectable> injectsKey;
382    final ImmutableList<Module> modules;
383    final ImmutableList<Object> expectedValues;
384
385    public SuccessTest(Builder builder) {
386      super("test");
387      name = builder.name;
388      key = builder.key;
389      injectsKey = builder.injectsKey;
390      modules = ImmutableList.copyOf(builder.modules);
391      expectedValues = ImmutableList.copyOf(builder.expectedValues);
392    }
393
394    public String getName() {
395      return name;
396    }
397
398    Injector newInjector() {
399      nextId.set(101);
400      return Guice.createInjector(modules);
401    }
402
403    public void test() throws IllegalAccessException, InstantiationException {
404      Injector injector = newInjector();
405      nextId.set(201);
406      for (Object value : expectedValues) {
407        assertEquals(value, injector.getInstance(key));
408      }
409
410      Provider<?> provider = newInjector().getProvider(key);
411      nextId.set(201);
412      for (Object value : expectedValues) {
413        assertEquals(value, provider.get());
414      }
415
416      Provider<?> bindingProvider = newInjector().getBinding(key).getProvider();
417      nextId.set(201);
418      for (Object value : expectedValues) {
419        assertEquals(value, bindingProvider.get());
420      }
421
422      injector = newInjector();
423      nextId.set(201);
424      for (Object value : expectedValues) {
425        Injectable instance = injector.getInstance(injectsKey);
426        assertEquals(value, instance.value);
427      }
428
429      injector = newInjector();
430      nextId.set(201);
431      for (Object value : expectedValues) {
432        Injectable injectable = injectsKey.newInstance();
433        injector.injectMembers(injectable);
434        assertEquals(value, injectable.value);
435      }
436
437      Injector injector1 = newInjector();
438      nextId.set(201);
439      Injectable hasProvider = injector1.getInstance(injectsKey);
440      hasProvider.provider.get();
441      nextId.set(201);
442      for (Object value : expectedValues) {
443        assertEquals(value, hasProvider.provider.get());
444      }
445    }
446  }
447
448  public static class CreationExceptionTest extends TestCase {
449    final String name;
450    final Key<?> key;
451    final ImmutableList<Module> modules;
452    final String creationException;
453
454    public CreationExceptionTest(Builder builder) {
455      super("test");
456      name = builder.name;
457      key = builder.key;
458      modules = ImmutableList.copyOf(builder.modules);
459      creationException = builder.creationException;
460    }
461
462    public String getName() {
463      return "creation errors:" + name;
464    }
465
466    public void test() {
467      try {
468        Guice.createInjector(modules);
469        fail();
470      } catch (CreationException expected) {
471        assertContains(expected.getMessage(), creationException);
472      }
473    }
474  }
475
476  public static class ConfigurationExceptionTest extends TestCase {
477    final String name;
478    final Key<?> key;
479    final Class<? extends Injectable> injectsKey;
480    final ImmutableList<Module> modules;
481    final String configurationException;
482
483    public ConfigurationExceptionTest(Builder builder) {
484      super("test");
485      name = builder.name;
486      key = builder.key;
487      injectsKey = builder.injectsKey;
488      modules = ImmutableList.copyOf(builder.modules);
489      configurationException = builder.configurationException;
490    }
491
492    public String getName() {
493      return "provision errors:" + name;
494    }
495
496    Injector newInjector() {
497      return Guice.createInjector(modules);
498    }
499
500    public void test() throws IllegalAccessException, InstantiationException {
501      try {
502        newInjector().getProvider(key);
503        fail();
504      } catch (ConfigurationException expected) {
505        assertContains(expected.getMessage(), configurationException);
506      }
507
508      try {
509        newInjector().getBinding(key).getProvider();
510        fail();
511      } catch (ConfigurationException expected) {
512        assertContains(expected.getMessage(), configurationException);
513      }
514
515      try {
516        newInjector().getInstance(key);
517        fail();
518      } catch (ConfigurationException expected) {
519        assertContains(expected.getMessage(), configurationException);
520      }
521
522      try {
523        newInjector().getInstance(injectsKey);
524        fail();
525      } catch (ConfigurationException expected) {
526        assertContains(expected.getMessage(),
527            configurationException, injectsKey.getName() + ".inject",
528            configurationException, injectsKey.getName() + ".inject",
529            "2 errors");
530      }
531
532      try {
533        Injectable injectable = injectsKey.newInstance();
534        newInjector().injectMembers(injectable);
535        fail();
536      } catch (ConfigurationException expected) {
537        assertContains(expected.getMessage(),
538            configurationException, injectsKey.getName() + ".inject",
539            configurationException, injectsKey.getName() + ".inject",
540            "2 errors");
541      }
542    }
543  }
544
545  public static class UserExceptionsTest extends TestCase {
546    final String name;
547    final Key<?> key;
548    final Class<? extends Injectable> injectsKey;
549    final ImmutableList<Module> modules;
550    final ImmutableList<Object> expectedValues;
551    final CreationTime creationTime;
552
553    public UserExceptionsTest(Builder builder) {
554      super("test");
555      name = builder.name;
556      key = builder.key;
557      injectsKey = builder.injectsKey;
558      modules = ImmutableList.copyOf(builder.modules);
559      expectedValues = ImmutableList.copyOf(builder.expectedValues);
560      creationTime = builder.creationTime;
561    }
562
563    public String getName() {
564      return "provision errors:" + name;
565    }
566
567    Injector newInjector() {
568      return Guice.createInjector(modules);
569    }
570
571    public void test() throws IllegalAccessException, InstantiationException {
572      nextId.set(-1);
573      try {
574        newInjector();
575        assertEquals(CreationTime.LAZY, creationTime);
576      } catch (CreationException expected) {
577        assertEquals(CreationTime.EAGER, creationTime);
578        assertContains(expected.getMessage(), "Illegal value: -1");
579        return;
580      }
581
582      Provider<?> provider = newInjector().getProvider(key);
583      Provider<?> bindingProvider = newInjector().getBinding(key).getProvider();
584
585      nextId.set(-1);
586      try {
587        newInjector().getInstance(key);
588        fail();
589      } catch (ProvisionException expected) {
590        assertContains(expected.getMessage(), "Illegal value: -1");
591      }
592
593      nextId.set(-1);
594      try {
595        provider.get();
596        fail();
597      } catch (ProvisionException expected) {
598        assertContains(expected.getMessage(), "Illegal value: -1");
599      }
600
601      nextId.set(-1);
602      try {
603        bindingProvider.get();
604        fail();
605      } catch (ProvisionException expected) {
606        assertContains(expected.getMessage(), "Illegal value: -1");
607      }
608
609      try {
610        nextId.set(-1);
611        newInjector().getInstance(injectsKey);
612      } catch (ProvisionException expected) {
613        assertContains(expected.getMessage(), "Illegal value: -1",
614            "for parameter 0 at " + injectsKey.getName() + ".inject");
615      }
616
617      nextId.set(201);
618      Injectable injectable = injectsKey.newInstance();
619      try {
620        nextId.set(-1);
621        newInjector().injectMembers(injectable);
622      } catch (ProvisionException expected) {
623        assertContains(expected.getMessage(), "Illegal value: -1",
624            "for parameter 0 at " + injectsKey.getName() + ".inject");
625      }
626
627      nextId.set(201);
628      Injectable hasProvider = newInjector().getInstance(injectsKey);
629      hasProvider.provider.get();
630      try {
631        nextId.set(-1);
632        hasProvider.provider.get();
633      } catch (ProvisionException expected) {
634        assertContains(expected.getMessage(), "Illegal value: -1");
635      }
636    }
637  }
638
639  /** negative to throw, 101... for eager singletons, 201... for everything else */
640  static final AtomicInteger nextId = new AtomicInteger();
641
642  @ProvidedBy(PlainAProvider.class)
643  interface AWithProvidedBy {}
644
645  static class InjectsAWithProvidedBy extends Injectable {
646    @Inject public void inject(AWithProvidedBy aWithProvidedBy,
647        Provider<AWithProvidedBy> aWithProvidedByProvider) {
648      this.value = aWithProvidedBy;
649      this.provider = aWithProvidedByProvider;
650    }
651  }
652
653  static class InjectsAWithProvidedByNamedApple extends Injectable {
654    @Inject public void inject(@Named("apple") AWithProvidedBy aWithProvidedBy,
655        @Named("apple") Provider<AWithProvidedBy> aWithProvidedByProvider) {
656      this.value = aWithProvidedBy;
657      this.provider = aWithProvidedByProvider;
658    }
659  }
660
661  @ImplementedBy(PlainA.class)
662  interface AWithImplementedBy {}
663
664  static class InjectsAWithImplementedBy extends Injectable {
665    @Inject public void inject(AWithImplementedBy aWithImplementedBy,
666        Provider<AWithImplementedBy> aWithImplementedByProvider) {
667      this.value = aWithImplementedBy;
668      this.provider = aWithImplementedByProvider;
669    }
670  }
671
672  static class InjectsAWithImplementedByNamedApple extends Injectable {
673    @Inject public void inject(@Named("apple") AWithImplementedBy aWithImplementedBy,
674        @Named("apple") Provider<AWithImplementedBy> aWithImplementedByProvider) {
675      this.value = aWithImplementedBy;
676      this.provider = aWithImplementedByProvider;
677    }
678  }
679
680  interface A extends AWithProvidedBy, AWithImplementedBy {}
681
682  static class InjectsA extends Injectable {
683    @Inject public void inject(A a, Provider<A> aProvider) {
684      this.value = a;
685      this.provider = aProvider;
686    }
687  }
688
689  static class PlainA implements A {
690    final int value;
691    PlainA() {
692      value = nextId.getAndIncrement();
693      if (value < 0) {
694        throw new RuntimeException("Illegal value: " + value);
695      }
696    }
697    PlainA(int value) {
698      this.value = value;
699    }
700    public boolean equals(Object obj) {
701      return obj instanceof PlainA
702          && value == ((PlainA) obj).value;
703    }
704    public int hashCode() {
705      return value;
706    }
707    public String toString() {
708      return "PlainA#" + value;
709    }
710  }
711
712  static class PlainAProvider implements Provider<A> {
713    public A get() {
714      return new PlainA();
715    }
716  }
717
718  static class InjectsPlainA extends Injectable {
719    @Inject public void inject(PlainA plainA, Provider<PlainA> plainAProvider) {
720      this.value = plainA;
721      this.provider = plainAProvider;
722    }
723  }
724
725  /** This scope hands out each value exactly twice  */
726  static class TwoAtATimeScope implements Scope {
727    public <T> Provider<T> scope(Key<T> key, final Provider<T> unscoped) {
728      return new Provider<T>() {
729        T instance;
730        public T get() {
731          if (instance == null) {
732            instance = unscoped.get();
733            return instance;
734          } else {
735            T result = instance;
736            instance = null;
737            return result;
738          }
739        }
740      };
741    }
742  }
743
744  @Target({ TYPE, METHOD }) @Retention(RUNTIME) @ScopeAnnotation
745  public @interface TwoAtATimeScoped {}
746
747  @TwoAtATimeScoped
748  static class ScopedA extends PlainA {}
749
750  static class InjectsScopedA extends Injectable {
751    @Inject public void inject(ScopedA scopedA, Provider<ScopedA> scopedAProvider) {
752      this.value = scopedA;
753      this.provider = scopedAProvider;
754    }
755  }
756
757  static class InjectsScopedANamedApple extends Injectable {
758    @Inject public void inject(@Named("apple") ScopedA scopedA,
759        @Named("apple") Provider<ScopedA> scopedAProvider) {
760      this.value = scopedA;
761      this.provider = scopedAProvider;
762    }
763  }
764
765  static class Injectable {
766    Object value = new Object();
767    Provider<?> provider = Providers.of(new Object());
768  }
769}
770