/*
* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.acai;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.acai.TestScope.TestScopeModule;
import com.google.acai.TestingServiceModule.NoopTestingServiceModule;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
/**
* Acai rule for integrating Guice with a JUnit4 test.
*
* <p>Use to inject a test with a module:
*
* <pre>
* public class MyTest {
* {@literal @}Rule Acai acai = new Acai(MyModule.class);
*
* {@literal @}Inject MyClass willBeInjectedUsingMyModule;
*
* {@literal @}Test
* public void testSomething() {
* // Your test goes here.
* }
* }
* </pre>
*
* <p>To configure services to run before or between tests see {@link TestingService} and {@link
* TestingServiceModule}.
*
* <p>See the {@code README.md} file for detailed usage examples.
*/
public class Acai implements MethodRule {
private static final Map<Class<? extends Module>, TestEnvironment> environments = new HashMap<>();
private static final Key<Set<TestingService>> TESTING_SERVICES_KEY =
new Key<Set<TestingService>>(AcaiInternal.class) {};
private final Class<? extends Module> module;
public Acai(Class<? extends Module> module) {
this.module = checkNotNull(module);
}
@Override
public Statement apply(
final Statement statement, FrameworkMethod frameworkMethod, final Object target) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
TestEnvironment testEnvironment = getOrCreateTestEnvironment(module);
testEnvironment.beforeSuiteIfNotAlreadyRun();
testEnvironment.enterTestScope();
try {
testEnvironment.beforeTest();
testEnvironment.inject(target);
try {
statement.evaluate();
} finally {
testEnvironment.afterTest();
}
} finally {
testEnvironment.exitTestScope();
}
}
};
}
/**
* Returns the {@code TestEnvironment} for the module, creating it if this is the first time it
* has been requested.
*/
private static TestEnvironment getOrCreateTestEnvironment(Class<? extends Module> module) {
if (environments.containsKey(module)) {
return environments.get(module);
}
Injector injector =
Guice.createInjector(
instantiateModule(module), new NoopTestingServiceModule(), new TestScopeModule());
TestEnvironment testEnvironment =
new TestEnvironment(
injector,
Dependencies.inOrder(injector.getInstance(TESTING_SERVICES_KEY))
.stream()
.map(TestingServiceManager::new)
.collect(Collectors.toList()));
environments.put(module, testEnvironment);
return testEnvironment;
}
/**
* Instantiates the module from its class ignoring visibility restrictions.
*
* <p>Will fail if the module does not have a zero argument constructor.
*/
private static Module instantiateModule(Class<? extends Module> module) {
try {
Constructor<? extends Module> constructor = module.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Module provided by user does not have zero argument constructor.", e);
} catch (InvocationTargetException e) {
if (e.getCause() != null) {
Throwables.throwIfUnchecked(e.getCause());
throw new RuntimeException(e.getCause());
}
throw new RuntimeException(e);
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* Resets the static environment map.
*
* <p>For use in unit tests of Acai itself.
*/
@VisibleForTesting
static void testOnlyResetEnvironments() {
environments.clear();
}
/**
* A TestEnvironment represents a configuration for running tests derived from a single Guice
* module.
*/
private static class TestEnvironment {
private final Injector injector;
private final ImmutableList<TestingServiceManager> testingServices;
private final AtomicBoolean beforeSuiteHasRun = new AtomicBoolean(false);
private final TestScope testScope;
/**
* Initializes a newly created {@code TestEnvironment} with an {@code injector} and a list of
* {@code testingServices} in the order they will be executed.
*/
TestEnvironment(Injector injector, Iterable<TestingServiceManager> testingServices) {
this.injector = checkNotNull(injector);
this.testingServices = ImmutableList.copyOf(testingServices);
this.testScope = injector.getInstance(Key.get(TestScope.class, AcaiInternal.class));
}
void inject(Object target) {
injector.injectMembers(target);
}
void beforeSuiteIfNotAlreadyRun() {
if (beforeSuiteHasRun.getAndSet(true)) {
return;
}
for (TestingServiceManager testingService : testingServices) {
testingService.beforeSuite();
}
}
void beforeTest() {
for (TestingServiceManager testingService : testingServices) {
testingService.beforeTest();
}
}
void enterTestScope() {
testScope.enter();
}
void exitTestScope() {
testScope.exit();
}
void afterTest() {
for (TestingServiceManager testingService : testingServices.reverse()) {
testingService.afterTest();
}
}
}
}