PageRenderTime 210ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/com/google/guiceberry/GuiceBerryUniverse.java

http://guiceberry.googlecode.com/
Java | 388 lines | 278 code | 53 blank | 57 comment | 32 complexity | e67754d2d49e2c14b8356601a3e8fc98 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright (C) 2010 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. package com.google.guiceberry;
  17. import com.google.common.base.Preconditions;
  18. import com.google.common.collect.Maps;
  19. import com.google.common.testing.TearDown;
  20. import com.google.common.testing.TearDownAccepter;
  21. import com.google.common.testing.TearDownStack;
  22. import com.google.guiceberry.GuiceBerry.GuiceBerryWrapper;
  23. import com.google.guiceberry.GuiceBerryModule.ToTearDown;
  24. import com.google.inject.AbstractModule;
  25. import com.google.inject.ConfigurationException;
  26. import com.google.inject.CreationException;
  27. import com.google.inject.Guice;
  28. import com.google.inject.Injector;
  29. import com.google.inject.Key;
  30. import com.google.inject.Module;
  31. import com.google.inject.testing.guiceberry.GuiceBerryEnv;
  32. import com.google.inject.testing.guiceberry.junit3.GuiceBerryJunit3;
  33. import java.util.Map;
  34. /**
  35. * @author Luiz-Otavio "Z" Zorzella
  36. */
  37. class GuiceBerryUniverse {
  38. static final GuiceBerryUniverse INSTANCE = new GuiceBerryUniverse();
  39. final Map<Class<? extends Module>, Injector> gbeClassToInjectorMap = Maps.newHashMap();
  40. public final InheritableThreadLocal<TestDescription> currentTestDescriptionThreadLocal =
  41. new InheritableThreadLocal<TestDescription>();
  42. /**
  43. * If something goes wrong trying to get an Injector instance for some
  44. * GuiceBerryEnv, this instance is stored in the
  45. * {@link GuiceBerryUniverse#gbeClassToInjectorMap}, to allow for graceful
  46. * error handling.
  47. */
  48. private static final Injector BOGUS_INJECTOR = Guice.createInjector(new GuiceBerryModule());
  49. /**
  50. * All bindings defined in {@link GuiceBerryModule}.
  51. */
  52. private static final Class<?>[] REQUIRED_BINDINGS = {
  53. TestScope.class,
  54. TearDownAccepter.class,
  55. ToTearDown.class,
  56. TestId.class
  57. };
  58. static class TestCaseScaffolding implements GuiceBerryWrapper {
  59. private final TestDescription testDescription;
  60. private final GuiceBerryEnvSelector guiceBerryEnvSelector;
  61. private final GuiceBerryUniverse universe;
  62. private Injector injector;
  63. private final TearDownStack stack = new TearDownStack();
  64. public TestCaseScaffolding(
  65. TestDescription testDescription,
  66. GuiceBerryEnvSelector guiceBerryEnvSelector,
  67. GuiceBerryUniverse universe) {
  68. this.testDescription = Preconditions.checkNotNull(testDescription);
  69. this.guiceBerryEnvSelector = Preconditions.checkNotNull(guiceBerryEnvSelector);
  70. this.universe = Preconditions.checkNotNull(universe);
  71. }
  72. public synchronized void runBeforeTest() {
  73. // If anything should go wrong, we "tag" this scaffolding as having failed
  74. // to acquire an injector, so that the tear down knows to skip the
  75. // appropriate steps.
  76. injector = BOGUS_INJECTOR;
  77. checkPreviousTestCalledTearDown(testDescription);
  78. final Class<? extends Module> gbeClass =
  79. guiceBerryEnvSelector.guiceBerryEnvToUse(testDescription);
  80. universe.currentTestDescriptionThreadLocal.set(testDescription);
  81. injector = getAndSetInjector(gbeClass);
  82. stack.addTearDown(new TearDown() {
  83. public void tearDown() throws Exception {
  84. doTearDown();
  85. }
  86. });
  87. stack.addTearDown(new TearDown() {
  88. public void tearDown() throws Exception {
  89. ToTearDown toTearDown = injector.getInstance(ToTearDown.class);
  90. toTearDown.runTearDown();
  91. }
  92. });
  93. TearDownAccepter accepter = wrappedGetInstance(injector, TearDownAccepter.class, gbeClass);
  94. buildTestWrapperInstance(injector).toRunBeforeTest();
  95. injectMembersIntoTest(gbeClass, injector);
  96. }
  97. /**
  98. * Throws an {@link IllegalArgumentException} if any of the bindings in
  99. * {@link GuiceBerryModule} is not defined in the given {@code injector},
  100. * that is, if the user has forgotten to install that module.
  101. *
  102. * <p>See {@link #throwAppropriateExceptionOnMissingRequiredBindings(Class)}.
  103. */
  104. private static void ensureBasicBindingsExist(Injector injector,
  105. Class<? extends Module> gbeClass) {
  106. for(Class<?> clazz : REQUIRED_BINDINGS) {
  107. if (!hasBinding(injector, clazz)) {
  108. throwAppropriateExceptionOnMissingRequiredBindings(gbeClass);
  109. }
  110. }
  111. }
  112. /**
  113. * Always throws an {@link IllegalArgumentException} exception telling the
  114. * user he/she forgot to install {@link GuiceBerryModule}.
  115. *
  116. * The given {@code gbeClass} is used to provide a good error message,
  117. * which includes the class name, as well being tailored for a gbe that
  118. * extends {@link GuiceBerryModule} or not.
  119. */
  120. private static void throwAppropriateExceptionOnMissingRequiredBindings(
  121. Class<? extends Module> gbeClass) {
  122. if (GuiceBerryModule.class.isAssignableFrom(gbeClass)) {
  123. throw new IllegalArgumentException(String.format(
  124. "The GuiceBerry Env '%s' must call 'super.configure()' in its "
  125. + "'configure()' method, so as to install the bindings defined"
  126. + " in GuiceBerryModule.", gbeClass.getName()));
  127. } else if (AbstractModule.class.isAssignableFrom(gbeClass)) {
  128. throw new IllegalArgumentException(String.format(
  129. "The GuiceBerry Env '%s' must call "
  130. + "'install(new GuiceBerryModule())' in its 'configure()'"
  131. + " method, so as to install the bindings defined there.",
  132. gbeClass.getName()));
  133. } else {
  134. throw new IllegalArgumentException(String.format(
  135. "The GuiceBerry Env '%s' must call "
  136. + "'binder.install(new GuiceBerryModule()' in its "
  137. + "'configure(Binder)' method, so as to install the bindings "
  138. + "defined there.", gbeClass.getName()));
  139. }
  140. }
  141. private void injectMembersIntoTest(
  142. final Class<? extends Module> gbeClass, Injector injector) {
  143. try {
  144. injector.injectMembers(testDescription.getTestCase());
  145. } catch (ConfigurationException e) {
  146. String msg = String.format("Binding error in the GuiceBerry Env '%s': '%s'.",
  147. gbeClass.getName(), e.getMessage());
  148. throw new RuntimeException(msg, e);
  149. }
  150. }
  151. private static <T> T wrappedGetInstance(
  152. final Injector injector,
  153. final Class<T> clazz,
  154. final Class<? extends Module> gbeClass
  155. ) {
  156. try {
  157. return injector.getInstance(clazz);
  158. } catch (ConfigurationException e) {
  159. String msg = String.format("Binding error in the GuiceBerry Env '%s': '%s'.",
  160. gbeClass.getName(), e.getMessage());
  161. throw new RuntimeException(msg, e);
  162. }
  163. }
  164. /**
  165. * Returns the {@link Injector} for the given {@code gbeClass}. If this
  166. * GuiceBerry env has never been seen before, add it to the
  167. * {@link #gbeClassToInjectorMap}.
  168. */
  169. private Injector getAndSetInjector(final Class<? extends Module> gbeClass) {
  170. synchronized (universe.gbeClassToInjectorMap) {
  171. if (!universe.gbeClassToInjectorMap.containsKey(gbeClass)) {
  172. foundGbeForTheFirstTime(gbeClass);
  173. }
  174. }
  175. Injector result = universe.gbeClassToInjectorMap.get(gbeClass);
  176. if (result == BOGUS_INJECTOR) {
  177. throw new RuntimeException(String.format(
  178. "Skipping '%s' GuiceBerryEnv which failed previously during injector creation.",
  179. gbeClass.getName()));
  180. }
  181. return result;
  182. }
  183. private void checkPreviousTestCalledTearDown(TestDescription testCase) {
  184. TestDescription previousTestCase = universe.currentTestDescriptionThreadLocal.get();
  185. if (previousTestCase != null) {
  186. String msg = String.format(
  187. "Error while setting up a test: GuiceBerry was asked to " +
  188. "set up test '%s', but the previous test '%s' did not properly " +
  189. "call GuiceBerry's tear down.",
  190. testCase.getName(),
  191. previousTestCase.getName());
  192. throw new RuntimeException(msg);
  193. }
  194. }
  195. private void foundGbeForTheFirstTime(final Class<? extends Module> gbeClass) {
  196. Injector result = BOGUS_INJECTOR;
  197. try {
  198. Module gbeInstance = createGbeInstanceFromClass(gbeClass);
  199. Injector injector = Guice.createInjector(gbeInstance);
  200. ensureBasicBindingsExist(injector, gbeClass);
  201. callGbeMainIfBound(injector);
  202. // We don't actually use the test wrapper here, but we make sure we can
  203. // get an instance (i.e. we fail fast).
  204. buildTestWrapperInstance(injector);
  205. result = injector;
  206. } catch (CreationException e) {
  207. if (e.getMessage().contains("No scope is bound to " + TestScoped.class.getName())) {
  208. throwAppropriateExceptionOnMissingRequiredBindings(gbeClass);
  209. } else {
  210. throw e;
  211. }
  212. } finally {
  213. // This is in the finally block to ensure that BOGUS_INJECTOR
  214. // is put in the map if things go bad.
  215. universe.gbeClassToInjectorMap.put(gbeClass, result);
  216. }
  217. }
  218. private static TestWrapper buildTestWrapperInstance(Injector injector) {
  219. TestWrapper result = NoOpTestScopeListener.NO_OP_INSTANCE;
  220. try {
  221. boolean hasTestScopeListenerBinding = hasTestScopeListenerBinding(injector);
  222. boolean hasDeprecatedTestScopeListenerBinding = hasDeprecatedTestScopeListenerBinding(injector);
  223. if (hasTestScopeListenerBinding && hasDeprecatedTestScopeListenerBinding) {
  224. throw new RuntimeException(
  225. "Your GuiceBerry Env has bindings for both the new TestScopeListener and the deprecated one. Please fix.");
  226. } else if (hasTestScopeListenerBinding) {
  227. result = injector.getInstance(TestWrapper.class);
  228. } else if (hasDeprecatedTestScopeListenerBinding) {
  229. result = adapt(
  230. injector.getInstance(com.google.inject.testing.guiceberry.TestScopeListener.class),
  231. injector.getInstance(TearDownAccepter.class));
  232. }
  233. } catch (ConfigurationException e) {
  234. String msg = String.format("Error while creating a TestWrapper: '%s'.",
  235. e.getMessage());
  236. throw new RuntimeException(msg, e);
  237. }
  238. return result;
  239. }
  240. private static TestWrapper adapt(
  241. final com.google.inject.testing.guiceberry.TestScopeListener instance,
  242. final TearDownAccepter tearDownAccepter) {
  243. return new TestWrapper() {
  244. public void toRunBeforeTest() {
  245. tearDownAccepter.addTearDown(new TearDown() {
  246. public void tearDown() throws Exception {
  247. instance.exitingScope();
  248. }
  249. });
  250. instance.enteringScope();
  251. }
  252. };
  253. }
  254. private static boolean hasBinding(Injector injector, Class<?> clazz) {
  255. return injector.getBindings().get(Key.get(clazz)) != null;
  256. }
  257. private static <T> T getInstanceIfHasBinding(Injector injector, Class<T> clazz) {
  258. if (hasBinding(injector, clazz)) {
  259. return injector.getInstance(clazz);
  260. }
  261. return null;
  262. }
  263. private static boolean hasDeprecatedTestScopeListenerBinding(Injector injector) {
  264. return hasBinding(injector, com.google.inject.testing.guiceberry.TestScopeListener.class);
  265. }
  266. private static boolean hasTestScopeListenerBinding(Injector injector) {
  267. return hasBinding(injector, TestWrapper.class);
  268. }
  269. private static void callGbeMainIfBound(Injector injector) {
  270. com.google.inject.testing.guiceberry.GuiceBerryEnvMain deprecatedGuiceBerryEnvMain =
  271. getInstanceIfHasBinding(injector, com.google.inject.testing.guiceberry.GuiceBerryEnvMain.class);
  272. GuiceBerryEnvMain guiceBerryEnvMain =
  273. getInstanceIfHasBinding(injector, GuiceBerryEnvMain.class);
  274. if ((deprecatedGuiceBerryEnvMain != null) && (guiceBerryEnvMain != null)) {
  275. throw new RuntimeException(String.format(
  276. "You have bound both the deprecated and the new versions of GuiceBerryEnvMain ('%s' and '%s'). "
  277. + "Please remove the binding to the deprecated one.",
  278. deprecatedGuiceBerryEnvMain.getClass().getName(),
  279. guiceBerryEnvMain.getClass().getName()));
  280. }
  281. if (deprecatedGuiceBerryEnvMain != null) {
  282. deprecatedGuiceBerryEnvMain.run();
  283. }
  284. if (guiceBerryEnvMain != null) {
  285. guiceBerryEnvMain.run();
  286. }
  287. }
  288. private static Module createGbeInstanceFromClass(final Class<? extends Module> gbeClass) {
  289. Module result;
  290. try {
  291. result = gbeClass.getConstructor().newInstance();
  292. } catch (NoSuchMethodException e) {
  293. String msg = String.format(
  294. "@%s class '%s' must have a public zero-arguments constructor",
  295. GuiceBerryEnv.class.getSimpleName(),
  296. gbeClass.getName());
  297. throw new IllegalArgumentException(msg, e);
  298. } catch (Exception e) {
  299. String msg = String.format(
  300. "Error creating instance of @%s '%s'",
  301. GuiceBerryEnv.class.getSimpleName(),
  302. gbeClass.getName());
  303. throw new IllegalArgumentException(msg, e);
  304. }
  305. return result;
  306. }
  307. public void runAfterTest() {
  308. if (injector == BOGUS_INJECTOR) {
  309. // We failed to get a valid injector for this module in the setUp method,
  310. // so we just gracefully return, after cleaning up the threadlocal (which
  311. // normally would happen in the doTearDown method).
  312. universe.currentTestDescriptionThreadLocal.remove();
  313. return;
  314. }
  315. stack.runTearDown();
  316. }
  317. private void doTearDown() {
  318. if (!universe.currentTestDescriptionThreadLocal.get().equals(testDescription)) {
  319. String msg = String.format(GuiceBerryJunit3.class.toString()
  320. + " cannot tear down "
  321. + testDescription.toString()
  322. + " because that test never called "
  323. + GuiceBerryJunit3.class.getCanonicalName()
  324. + ".setUp()");
  325. throw new RuntimeException(msg);
  326. }
  327. universe.currentTestDescriptionThreadLocal.remove();
  328. injector.getInstance(TestScope.class).finishScope(testDescription);
  329. }
  330. }
  331. private static final class NoOpTestScopeListener implements TestWrapper {
  332. private static final TestWrapper NO_OP_INSTANCE = new NoOpTestScopeListener();
  333. public void toRunBeforeTest() {}
  334. }
  335. }