package net.thucydides.core.steps;
import com.google.common.base.Preconditions;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.thucydides.core.IgnoredStepException;
import net.thucydides.core.PendingStepException;
import net.thucydides.core.annotations.Pending;
import net.thucydides.core.annotations.Step;
import net.thucydides.core.annotations.StepGroup;
import net.thucydides.core.annotations.TestAnnotations;
import org.junit.internal.AssumptionViolatedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import static net.thucydides.core.steps.ErrorConvertor.forError;
import static org.apache.commons.lang3.StringUtils.split;
/**
* Listen to step results and publish notification messages.
* The step interceptor is designed to work on a given test case or user story.
* It logs test step results so that they can be reported on at the end of the test case.
*
* @author johnsmart
*/
public class StepInterceptor implements MethodInterceptor, Serializable {
private static final long serialVersionUID = 1L;
private final Class<?> testStepClass;
private Throwable error = null;
private static final Logger LOGGER = LoggerFactory.getLogger(StepInterceptor.class);
private boolean throwExceptionImmediately = false;
public StepInterceptor(final Class<?> testStepClass) {
this.testStepClass = testStepClass;
}
public Object intercept(final Object obj, final Method method,
final Object[] args, final MethodProxy proxy) throws Throwable {
Object result;
if (baseClassMethod(method, obj.getClass())) {
result = runBaseObjectMethod(obj, method, args, proxy);
} else {
result = testStepResult(obj, method, args, proxy);
}
return result;
}
private final List<String> OBJECT_METHODS
= Arrays.asList("toString",
"equals",
"hashcode",
"clone",
"notify",
"notifyAll",
"wait",
"finalize",
"getMetaClass");
private boolean baseClassMethod(final Method method, final Class callingClass) {
boolean isACoreLanguageMethod = (OBJECT_METHODS.contains(method.getName()));
boolean methodDoesNotComeFromThisClassOrARelatedParentClass = !declaredInSameDomain(method, callingClass);
return (isACoreLanguageMethod || methodDoesNotComeFromThisClassOrARelatedParentClass);
}
private boolean declaredInSameDomain(Method method, final Class callingClass) {
return domainPackageOf(getRoot(method)).equals(domainPackageOf(callingClass));
}
private String domainPackageOf(Class callingClass) {
Package classPackage = callingClass.getPackage();
String classPackageName = (classPackage != null) ? classPackage.getName() : "";
return packageDomainName(classPackageName);
}
private String packageDomainName(String methodPackage) {
String[] packages = split(methodPackage, ".");
if (packages.length == 0) {
return "";
} else if (packages.length == 1) {
return packages[0];
} else {
return packages[0] + "." + packages[1];
}
}
private String domainPackageOf(Method method) {
Package methodPackage = method.getDeclaringClass().getPackage();
String methodPackageName = (methodPackage != null) ? methodPackage.getName() : "";
return packageDomainName(methodPackageName);
}
private Method getRoot(Method method) {
try {
method.getClass().getDeclaredField("root").setAccessible(true);
return (Method) method.getClass().getDeclaredField("root").get(method);
} catch (IllegalAccessException e) {
return method;
} catch (NoSuchFieldException e) {
return method;
}
}
private Object testStepResult(final Object obj, final Method method,
final Object[] args, final MethodProxy proxy) throws Throwable {
if (!isATestStep(method)) {
return runNormalMethod(obj, method, args, proxy);
}
if (shouldSkip(method)) {
notifySkippedStepStarted(method, args);
return skipTestStep(obj, method, args, proxy);
} else {
notifyStepStarted(method, args);
return runTestStep(obj, method, args, proxy);
}
}
private Object skipTestStep(Object obj, Method method, Object[] args, MethodProxy proxy) throws Exception {
Object skippedReturnObject = runSkippedMethod(obj, method, args, proxy);
notifyStepSkippedFor(method, args);
LOGGER.info("SKIPPED STEP: {}", method.getName());
return appropriateReturnObject(skippedReturnObject, obj, method);
}
private Object runSkippedMethod(Object obj, Method method, Object[] args, MethodProxy proxy) {
LOGGER.trace("Running test step " + getTestNameFrom(method, args, false));
Object result = null;
StepEventBus.getEventBus().temporarilySuspendWebdriverCalls();
result = runIfNestedMethodsShouldBeRun(obj, method, args, proxy);
StepEventBus.getEventBus().reenableWebdriverCalls();
return result;
}
private Object runIfNestedMethodsShouldBeRun(Object obj, Method method, Object[] args, MethodProxy proxy) {
Object result = null;
try {
if (!TestAnnotations.shouldSkipNested(method)) {
result = invokeMethod(obj, args, proxy);
}
} catch (Throwable anyException) {
LOGGER.trace("Ignoring exception thrown during a skipped test", anyException);
}
return result;
}
Object appropriateReturnObject(final Object returnedValue, final Object obj, final Method method) {
if (returnedValue != null) {
return returnedValue;
} else {
return appropriateReturnObject(obj, method);
}
}
Object appropriateReturnObject(final Object obj, final Method method) {
if (method.getReturnType().isAssignableFrom(obj.getClass())) {
return obj;
} else {
return null;
}
}
private boolean shouldNotSkipMethod(final Method methodOrStep, final Class callingClass) {
return !shouldSkipMethod(methodOrStep, callingClass);
}
private boolean shouldSkipMethod(final Method methodOrStep, final Class callingClass) {
return ((aPreviousStepHasFailed() || testIsPending()) && declaredInSameDomain(methodOrStep, callingClass));
}
private boolean shouldSkip(final Method methodOrStep) {
return aPreviousStepHasFailed() || testIsPending() || isPending(methodOrStep) || isIgnored(methodOrStep);
}
private boolean testIsPending() {
return StepEventBus.getEventBus().currentTestIsSuspended();
}
private boolean aPreviousStepHasFailed() {
boolean aPreviousStepHasFailed = false;
if (StepEventBus.getEventBus().aStepInTheCurrentTestHasFailed()) {
aPreviousStepHasFailed = true;
}
return aPreviousStepHasFailed;
}
private Object runBaseObjectMethod(final Object obj, final Method method, final Object[] args, final MethodProxy proxy)
throws Throwable {
return invokeMethod(obj, args, proxy);
}
private Object runNormalMethod(final Object obj, final Method method, final Object[] args, final MethodProxy proxy)
throws Throwable {
Object result = defaultReturnValueFor(method);
if (shouldNotSkipMethod(method, obj.getClass())) {
result = invokeMethodAndNotifyFailures(obj, method, args, proxy, result);
}
return result;
}
private Object invokeMethodAndNotifyFailures(Object obj, Method method, Object[] args, MethodProxy proxy, Object result) throws Throwable {
try {
result = invokeMethod(obj, args, proxy);
} catch (Throwable generalException) {
error = generalException;
Throwable assertionError = forError(error).convertToAssertion();
notifyStepStarted(method, args);
notifyOfStepFailure(method, args, assertionError);
}
return result;
}
private Object defaultReturnValueFor(Method method) {
if (method.getReturnType() == method.getDeclaringClass()) {
return this;
} else {
return DefaultValue.forClass(method.getReturnType());
}
}
private boolean isAnnotatedWithAValidStepAnnotation(final Method method) {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (isAThucydidesStep(annotation) || (AnnotatedStepDescription.isACompatibleStep(annotation))) {
return true;
}
}
return false;
}
private boolean isAThucydidesStep(Annotation annotation) {
return (annotation instanceof Step) || (annotation instanceof StepGroup);
}
private boolean isATestStep(final Method method) {
return isAnnotatedWithAValidStepAnnotation(method);
}
private boolean isIgnored(final Method method) {
return TestAnnotations.isIgnored(method);
}
private Object runTestStep(final Object obj, final Method method,
final Object[] args, final MethodProxy proxy) throws Throwable {
LOGGER.info("STARTING STEP: {}", method.getName());
Object result = null;
try {
result = executeTestStepMethod(obj, method, args, proxy, result);
LOGGER.info("STEP DONE: {}", method.getName());
} catch (AssertionError failedAssertion) {
error = failedAssertion;
logStepFailure(method, args, failedAssertion);
return appropriateReturnObject(obj, method);
} catch (AssumptionViolatedException assumptionFailed) {
return appropriateReturnObject(obj, method);
} catch (Throwable testErrorException) {
error = testErrorException;
logStepFailure(method, args, forError(error).convertToAssertion());
return appropriateReturnObject(obj, method);
}
return result;
}
private void logStepFailure(Method method, Object[] args, Throwable assertionError) throws Throwable {
notifyOfStepFailure(method, args, assertionError);
LOGGER.info("STEP FAILED: {} - {}", method.getName(), assertionError.getMessage());
}
private Object executeTestStepMethod(Object obj, Method method, Object[] args, MethodProxy proxy, Object result) throws Throwable {
try {
result = invokeMethod(obj, args, proxy);
notifyStepFinishedFor(method, args);
} catch(PendingStepException pendingStep) {
notifyStepPending(pendingStep.getMessage());
} catch(IgnoredStepException ignoredStep) {
notifyStepIgnored(ignoredStep.getMessage());
} catch(AssumptionViolatedException assumptionViolated) {
notifyAssumptionViolated(assumptionViolated.getMessage());
}
Preconditions.checkArgument(true);
return result;
}
private Object invokeMethod(final Object obj, final Object[] args, final MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
private boolean isPending(final Method method) {
return (method.getAnnotation(Pending.class) != null);
}
private void notifyStepFinishedFor(final Method method, final Object[] args) {
StepEventBus.getEventBus().stepFinished();
}
private void notifyStepPending(String message) {
StepEventBus.getEventBus().stepPending(message);
}
private void notifyAssumptionViolated(String message) {
StepEventBus.getEventBus().assumptionViolated(message);
}
private void notifyStepIgnored(String message) {
StepEventBus.getEventBus().stepIgnored();
}
private String getTestNameFrom(final Method method, final Object[] args) {
return getTestNameFrom(method, args, true);
}
private String getTestNameFrom(final Method method, final Object[] args, final boolean addMarkup) {
if ((args == null) || (args.length == 0)) {
return method.getName();
} else {
return testNameWithArguments(method, args, addMarkup);
}
}
private String testNameWithArguments(final Method method,
final Object[] args,
final boolean addMarkup) {
StringBuilder testName = new StringBuilder(method.getName());
testName.append(": ");
if (addMarkup) {
testName.append("<span class='step-parameter'>");
}
boolean isFirst = true;
for (Object arg : args) {
if (!isFirst) {
testName.append(", ");
}
testName.append(StepArgumentWriter.readableFormOf(arg));
isFirst = false;
}
if (addMarkup) {
testName.append("</span>");
}
return testName.toString();
}
private void notifyStepSkippedFor(final Method method, final Object[] args)
throws Exception {
if (isPending(method)) {
StepEventBus.getEventBus().stepPending();
} else {
StepEventBus.getEventBus().stepIgnored();
}
}
private void notifyOfStepFailure(final Method method, final Object[] args,
final Throwable cause) throws Throwable {
ExecutedStepDescription description = ExecutedStepDescription.of(testStepClass, getTestNameFrom(method, args));
StepFailure failure = new StepFailure(description, cause);
StepEventBus.getEventBus().stepFailed(failure);
if (shouldThrowExceptionImmediately()) {
throw cause;
}
}
private boolean shouldThrowExceptionImmediately() {
return throwExceptionImmediately;
}
private void notifyStepStarted(final Method method, final Object[] args) {
ExecutedStepDescription description = ExecutedStepDescription.of(testStepClass, getTestNameFrom(method, args));
StepEventBus.getEventBus().stepStarted(description);
}
private void notifySkippedStepStarted(final Method method, final Object[] args) {
ExecutedStepDescription description = ExecutedStepDescription.of(testStepClass, getTestNameFrom(method, args));
StepEventBus.getEventBus().skippedStepStarted(description);
}
public void setThowsExceptionImmediately(boolean throwExceptionImmediately) {
this.throwExceptionImmediately = throwExceptionImmediately;
}
}