package au.com.dius.pact.provider.junit;
import au.com.dius.pact.model.Interaction;
import au.com.dius.pact.model.Pact;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestClassAwareTarget;
import au.com.dius.pact.provider.junit.target.TestTarget;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.internal.runners.statements.Fail;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.internal.runners.statements.RunBefores;
import org.junit.rules.RunRules;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_METHOD_VALIDATOR;
import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_VALIDATOR;
/**
* Internal class to support pact test running
* <p>
* Developed with {@link org.junit.runners.BlockJUnit4ClassRunner} in mind
*/
class InteractionRunner extends Runner {
private final TestClass testClass;
private final Pact pact;
private final ConcurrentHashMap<Interaction, Description> childDescriptions = new ConcurrentHashMap<>();
public InteractionRunner(final TestClass testClass, final Pact pact) throws InitializationError {
this.testClass = testClass;
this.pact = pact;
validate();
}
@Override
public Description getDescription() {
final Description description = Description.createSuiteDescription(testClass.getJavaClass());
for (Interaction i: pact.getInteractions()) {
description.addChild(describeChild(i));
}
return description;
}
protected Description describeChild(final Interaction interaction) {
if (!childDescriptions.containsKey(interaction)) {
childDescriptions.put(interaction, Description.createTestDescription(testClass.getJavaClass(),
pact.getConsumer().getName() + " - " + interaction.getDescription()));
}
return childDescriptions.get(interaction);
}
// Validation
protected void validate() throws InitializationError {
final List<Throwable> errors = new ArrayList<>();
validatePublicVoidNoArgMethods(Before.class, false, errors);
validatePublicVoidNoArgMethods(After.class, false, errors);
validatePublicVoidNoArgMethods(State.class, false, errors);
validateConstructor(errors);
validateTestTarget(errors);
validateRules(errors);
validateTargetRequestFilters(errors);
if (!errors.isEmpty()) {
throw new InitializationError(errors);
}
}
private void validateTargetRequestFilters(final List<Throwable> errors) {
testClass.getAnnotatedMethods(TargetRequestFilter.class)
.stream().forEach(method -> {
method.validatePublicVoid(false, errors);
if (method.getMethod().getParameterTypes().length != 1) {
errors.add(new Exception("Method " + method.getName() + " should take only a single HttpRequest parameter"));
} else if (!HttpRequest.class.isAssignableFrom(method.getMethod().getParameterTypes()[0])) {
errors.add(new Exception("Method " + method.getName() + " should take only a single HttpRequest parameter"));
}
});
}
protected void validatePublicVoidNoArgMethods(final Class<? extends Annotation> annotation, final boolean isStatic, final List<Throwable> errors) {
testClass.getAnnotatedMethods(annotation).stream().forEach(method -> method.validatePublicVoidNoArg(isStatic, errors));
}
protected void validateConstructor(final List<Throwable> errors) {
if (!hasOneConstructor()) {
errors.add(new Exception("Test class should have exactly one public constructor"));
}
if (!testClass.isANonStaticInnerClass()
&& hasOneConstructor()
&& (testClass.getOnlyConstructor().getParameterTypes().length != 0)) {
errors.add(new Exception("Test class should have exactly one public zero-argument constructor"));
}
}
protected boolean hasOneConstructor() {
return testClass.getJavaClass().getConstructors().length == 1;
}
protected void validateTestTarget(final List<Throwable> errors) {
final List<FrameworkField> annotatedFields = testClass.getAnnotatedFields(TestTarget.class);
if (annotatedFields.size() != 1) {
errors.add(new Exception("Test class should have exactly one field annotated with " + TestTarget.class.getName()));
} else if (!Target.class.isAssignableFrom(annotatedFields.get(0).getType())) {
errors.add(new Exception("Field annotated with " + TestTarget.class.getName() + " should implement " + Target.class.getName() + " interface"));
}
}
protected void validateRules(final List<Throwable> errors) {
RULE_VALIDATOR.validate(testClass, errors);
RULE_METHOD_VALIDATOR.validate(testClass, errors);
}
// Running
public void run(final RunNotifier notifier) {
for (final Interaction interaction : pact.getInteractions()) {
final Description description = describeChild(interaction);
notifier.fireTestStarted(description);
try {
interactionBlock(interaction).evaluate();
} catch (final Throwable e) {
notifier.fireTestFailure(new Failure(description, e));
} finally {
notifier.fireTestFinished(description);
}
}
}
protected Object createTest() throws Exception {
return testClass.getOnlyConstructor().newInstance();
}
protected Statement interactionBlock(final Interaction interaction) {
//1. prepare object
//2. get Target
//3. run Rule`s
//4. run Before`s
//5. run OnStateChange`s
//6. run test
//7. run After`s
final Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
final Target target = testClass.getAnnotatedFieldValues(test, TestTarget.class, Target.class).get(0);
if (target instanceof TestClassAwareTarget) {
((TestClassAwareTarget) target).setTestClass(testClass, test);
}
Statement statement = new Statement() {
@Override
public void evaluate() throws Throwable {
target.testInteraction(pact.getConsumer().getName(), interaction);
}
};
statement = withStateChanges(interaction, test, statement);
statement = withBefores(interaction, test, statement);
statement = withRules(interaction, test, statement);
statement = withAfters(interaction, test, statement);
return statement;
}
protected Statement withStateChanges(final Interaction interaction, final Object target, final Statement statement) {
if (StringUtils.isNotEmpty(interaction.getProviderState())) {
final String state = interaction.getProviderState();
final List<FrameworkMethod> onStateChange = new ArrayList<FrameworkMethod>();
for (FrameworkMethod ann: testClass.getAnnotatedMethods(State.class)) {
for(String annotationState : ann.getAnnotation(State.class).value()) {
if(annotationState.equalsIgnoreCase(state)) {
onStateChange.add(ann);
break;
}
}
}
if (onStateChange.isEmpty()) {
return new Fail(new MissingStateChangeMethod("MissingStateChangeMethod: Did not find a test class method annotated with @State(\"" + state + "\")"));
}
return new RunBefores(statement, onStateChange, target);
} else {
return statement;
}
}
protected Statement withBefores(final Interaction interaction, final Object target, final Statement statement) {
final List<FrameworkMethod> befores = testClass.getAnnotatedMethods(Before.class);
return befores.isEmpty() ? statement : new RunBefores(statement, befores, target);
}
protected Statement withAfters(final Interaction interaction, final Object target, final Statement statement) {
final List<FrameworkMethod> afters = testClass.getAnnotatedMethods(After.class);
return afters.isEmpty() ? statement : new RunAfters(statement, afters, target);
}
protected Statement withRules(final Interaction interaction, final Object target, final Statement statement) {
final List<TestRule> testRules = testClass.getAnnotatedMethodValues(target, Rule.class, TestRule.class);
testRules.addAll(testClass.getAnnotatedFieldValues(target, Rule.class, TestRule.class));
return testRules.isEmpty() ? statement : new RunRules(statement, testRules, describeChild(interaction));
}
}