/*
* Copyright (c) 2008-2017, Hazelcast, 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.hazelcast.test;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.test.annotation.Repeat;
import com.hazelcast.test.bounce.BounceMemberRule;
import com.hazelcast.util.EmptyStatement;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.utility.JavaModule;
import org.junit.After;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.internal.runners.statements.RunBefores;
import org.junit.rules.TestRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
import java.lang.annotation.Annotation;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static com.hazelcast.test.TestEnvironment.isRunningCompatibilityTest;
import static java.lang.Integer.getInteger;
/**
* Base test runner which has base system properties and test repetition logic.
*
* The tests are run in random order.
*/
public abstract class AbstractHazelcastClassRunner extends AbstractParameterizedHazelcastClassRunner {
private static final int DEFAULT_TEST_TIMEOUT_IN_SECONDS = getInteger("hazelcast.test.defaultTestTimeoutInSeconds", 300);
private static final boolean THREAD_DUMP_ON_FAILURE;
private static final ThreadLocal<String> TEST_NAME_THREAD_LOCAL = new InheritableThreadLocal<String>();
private static final boolean THREAD_CPU_TIME_INFO_AVAILABLE;
private static final boolean THREAD_CONTENTION_INFO_AVAILABLE;
static {
initialize();
final String threadDumpOnFailure = System.getProperty("hazelcast.test.threadDumpOnFailure");
THREAD_DUMP_ON_FAILURE = threadDumpOnFailure != null
? Boolean.parseBoolean(threadDumpOnFailure) : JenkinsDetector.isOnJenkins();
// randomize multicast group
Random rand = new Random();
int g1 = rand.nextInt(255);
int g2 = rand.nextInt(255);
int g3 = rand.nextInt(255);
System.setProperty("hazelcast.multicast.group", "224." + g1 + "." + g2 + "." + g3);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
boolean threadCPUTimeInfoAvailable = false;
if (threadMXBean.isThreadCpuTimeSupported()) {
try {
threadMXBean.setThreadCpuTimeEnabled(true);
threadCPUTimeInfoAvailable = true;
} catch (Throwable t) {
EmptyStatement.ignore(t);
}
}
THREAD_CPU_TIME_INFO_AVAILABLE = threadCPUTimeInfoAvailable;
boolean threadContentionInfoAvailable = false;
if (threadMXBean.isThreadContentionMonitoringSupported()) {
try {
threadMXBean.setThreadContentionMonitoringEnabled(true);
threadContentionInfoAvailable = true;
} catch (Throwable t) {
EmptyStatement.ignore(t);
}
}
THREAD_CONTENTION_INFO_AVAILABLE = threadContentionInfoAvailable;
}
// initialize environment, logging and attach a final-modifier removing agent if required
private static void initialize() {
if (isRunningCompatibilityTest()) {
attachFinalRemovalAgent();
System.out.println("Running compatibility tests.");
// Mock network cannot be used for compatibility testing
System.setProperty(TestEnvironment.HAZELCAST_TEST_USE_NETWORK, "true");
} else {
TestLoggingUtils.initializeLogging();
if (System.getProperty(TestEnvironment.HAZELCAST_TEST_USE_NETWORK) == null) {
System.setProperty(TestEnvironment.HAZELCAST_TEST_USE_NETWORK, "false");
}
}
System.setProperty("hazelcast.phone.home.enabled", "false");
System.setProperty("hazelcast.mancenter.enabled", "false");
System.setProperty("hazelcast.wait.seconds.before.join", "1");
System.setProperty("hazelcast.local.localAddress", "127.0.0.1");
System.setProperty("java.net.preferIPv4Stack", "true");
}
// When running a compatibility test, all com.hazelcast.* classes are transformed so that none are
// loaded with final modifier to allow subclass proxying.
private static void attachFinalRemovalAgent() {
Instrumentation instrumentation = ByteBuddyAgent.install();
new AgentBuilder.Default()
.type(new ElementMatcher<TypeDescription>() {
@Override
public boolean matches(TypeDescription target) {
return target.getName().startsWith("com.hazelcast");
}
})
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader, JavaModule module) {
int actualModifiers = typeDescription.getActualModifiers(false);
// unset final modifier
int nonFinalModifiers = actualModifiers & ~Opcodes.ACC_FINAL;
return builder.modifiers(nonFinalModifiers);
}
})
.installOn(instrumentation);
}
/**
* Creates a BlockJUnit4ClassRunner to run {@code clazz}
*
* @throws org.junit.runners.model.InitializationError if the test class is malformed.
*/
public AbstractHazelcastClassRunner(Class<?> clazz) throws InitializationError {
super(clazz);
}
public AbstractHazelcastClassRunner(Class<?> clazz, Object[] parameters, String name) throws InitializationError {
super(clazz, parameters, name);
}
public static String getTestMethodName() {
return TEST_NAME_THREAD_LOCAL.get();
}
static void setThreadLocalTestMethodName(String name) {
TestLoggingUtils.setThreadLocalTestMethodName(name);
TEST_NAME_THREAD_LOCAL.set(name);
}
static void removeThreadLocalTestMethodName() {
TestLoggingUtils.removeThreadLocalTestMethodName();
TEST_NAME_THREAD_LOCAL.remove();
}
@Override
protected List<FrameworkMethod> getChildren() {
List<FrameworkMethod> children = super.getChildren();
List<FrameworkMethod> modifiableList = new ArrayList<FrameworkMethod>(children);
Collections.shuffle(modifiableList);
return modifiableList;
}
@Override
@SuppressWarnings("deprecation")
protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) {
long timeout = getTimeout(method.getAnnotation(Test.class));
return new FailOnTimeoutStatement(method.getName(), next, timeout);
}
private long getTimeout(Test annotation) {
if (annotation == null || annotation.timeout() == 0) {
return TimeUnit.SECONDS.toMillis(DEFAULT_TEST_TIMEOUT_IN_SECONDS);
}
return annotation.timeout();
}
@Override
protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) {
List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(After.class);
Statement nextStatement = statement;
List<TestRule> testRules = getTestRules(target);
if (!testRules.isEmpty()) {
for (TestRule rule : testRules) {
if (rule instanceof BounceMemberRule) {
nextStatement = ((BounceMemberRule) rule).stopBouncing(statement);
}
}
}
if (THREAD_DUMP_ON_FAILURE) {
return new ThreadDumpAwareRunAfters(method, nextStatement, afters, target);
}
if (afters.isEmpty()) {
return nextStatement;
} else {
return new RunAfters(nextStatement, afters, target);
}
}
// Override withBefores to accommodate spawning the member bouncing thread after @Before's have been executed and before @Test
// when MemberBounceRule is in use
@Override
protected Statement withBefores(FrameworkMethod method, Object target,
Statement statement) {
List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(
Before.class);
List<TestRule> testRules = getTestRules(target);
Statement nextStatement = statement;
if (!testRules.isEmpty()) {
for (TestRule rule : testRules) {
if (rule instanceof BounceMemberRule) {
nextStatement = ((BounceMemberRule) rule).startBouncing(statement);
}
}
}
return befores.isEmpty() ? nextStatement : new RunBefores(nextStatement,
befores, target);
}
@Override
protected Statement methodBlock(FrameworkMethod method) {
Statement statement = super.methodBlock(method);
Repeat repeatable = getRepeatable(method);
if (repeatable == null || repeatable.value() < 2) {
return statement;
}
return new TestRepeater(statement, method.getMethod(), repeatable.value());
}
/**
* Gets the {@link Repeat} annotation if set.
*
* Method level definition overrides class level definition.
*/
private Repeat getRepeatable(FrameworkMethod method) {
Repeat repeatable = method.getAnnotation(Repeat.class);
if (repeatable == null) {
repeatable = super.getTestClass().getJavaClass().getAnnotation(Repeat.class);
}
return repeatable;
}
@Override
protected List<TestRule> getTestRules(Object target) {
List<TestRule> testRules = super.getTestRules(target);
Set<Class<? extends TestRule>> testRuleClasses = new HashSet<Class<? extends TestRule>>();
TestClass testClass = getTestClass();
// find the required test rule classes from test class itself
Annotation[] classAnnotations = testClass.getAnnotations();
for (Annotation annotation : classAnnotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
AutoRegisteredTestRule autoFilterRule = annotationType.getAnnotation(AutoRegisteredTestRule.class);
if (autoFilterRule != null) {
Class<? extends TestRule> testRuleClass = autoFilterRule.testRule();
testRuleClasses.add(testRuleClass);
}
}
// find the required test rule classes from methods
List<FrameworkMethod> annotatedMethods = testClass.getAnnotatedMethods();
for (FrameworkMethod annotatedMethod : annotatedMethods) {
Annotation[] methodAnnotations = annotatedMethod.getAnnotations();
for (Annotation annotation : methodAnnotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
AutoRegisteredTestRule autoFilterRule = annotationType.getAnnotation(AutoRegisteredTestRule.class);
if (autoFilterRule != null) {
Class<? extends TestRule> testRuleClass = autoFilterRule.testRule();
testRuleClasses.add(testRuleClass);
}
}
}
// create and register test rules
for (Class<? extends TestRule> testRuleClass : testRuleClasses) {
try {
TestRule testRule = testRuleClass.newInstance();
testRules.add(testRule);
} catch (Throwable t) {
System.err.println("Unable to create test rule instance of test rule class "
+ testRuleClass.getName() + " because of " + t);
}
}
return testRules;
}
@Override
protected Statement withAfterClasses(Statement statement) {
final Statement originalStatement = super.withAfterClasses(statement);
return new Statement() {
@Override
public void evaluate() throws Throwable {
originalStatement.evaluate();
Set<HazelcastInstance> instances = Hazelcast.getAllHazelcastInstances();
if (!instances.isEmpty()) {
String message = "Instances haven't been shut down: " + instances;
Hazelcast.shutdownAll();
throw new IllegalStateException(message);
}
}
};
}
private String generateThreadDump() {
StringBuilder dump = new StringBuilder();
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);
long currentThreadId = Thread.currentThread().getId();
for (ThreadInfo threadInfo : threadInfos) {
long threadId = threadInfo.getThreadId();
if (threadId == currentThreadId) {
continue;
}
dump.append('"');
dump.append(threadInfo.getThreadName());
dump.append("\" ");
Thread.State state = threadInfo.getThreadState();
dump.append("\n\tjava.lang.Thread.State: ");
dump.append(state);
if (threadInfo.getLockName() != null) {
dump.append(", on lock=").append(threadInfo.getLockName());
}
if (threadInfo.getLockOwnerName() != null) {
dump.append(", owned by ").append(threadInfo.getLockOwnerName());
dump.append(", id=").append(threadInfo.getLockOwnerId());
}
if (THREAD_CPU_TIME_INFO_AVAILABLE) {
dump.append(", cpu=").append(threadMXBean.getThreadCpuTime(threadId)).append(" nsecs");
dump.append(", usr=").append(threadMXBean.getThreadUserTime(threadId)).append(" nsecs");
}
if (THREAD_CONTENTION_INFO_AVAILABLE) {
dump.append(", blocked=").append(threadInfo.getBlockedTime()).append(" msecs");
dump.append(", waited=").append(threadInfo.getWaitedTime()).append(" msecs");
}
StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
for (StackTraceElement stackTraceElement : stackTraceElements) {
dump.append("\n\t\tat ");
dump.append(stackTraceElement);
}
dump.append("\n\n");
}
return dump.toString();
}
protected class ThreadDumpAwareRunAfters extends Statement {
private final FrameworkMethod method;
private final Statement next;
private final Object target;
private final List<FrameworkMethod> afters;
ThreadDumpAwareRunAfters(FrameworkMethod method, Statement next, List<FrameworkMethod> afters, Object target) {
this.method = method;
this.next = next;
this.afters = afters;
this.target = target;
}
@Override
public void evaluate() throws Throwable {
List<Throwable> errors = new ArrayList<Throwable>();
try {
next.evaluate();
} catch (Throwable e) {
if (!isJUnitAssumeException(e)) {
System.err.println("THREAD DUMP FOR TEST FAILURE: \"" + e.getMessage()
+ "\" at \"" + method.getName() + "\"\n");
try {
System.err.println(generateThreadDump());
} catch (Throwable t) {
System.err.println("Unable to get thread dump!");
e.printStackTrace();
}
}
errors.add(e);
} finally {
for (FrameworkMethod each : afters) {
try {
each.invokeExplosively(target);
} catch (Throwable e) {
errors.add(e);
}
}
}
MultipleFailureException.assertEmpty(errors);
}
private boolean isJUnitAssumeException(Throwable e) {
return e instanceof AssumptionViolatedException;
}
}
private class TestRepeater extends Statement {
private final Statement statement;
private final Method testMethod;
private final int repeat;
TestRepeater(Statement statement, Method testMethod, int repeat) {
this.statement = statement;
this.testMethod = testMethod;
this.repeat = Math.max(1, repeat);
}
/**
* Invokes the next {@link Statement statement} in the execution chain for the specified repeat count.
*/
@Override
public void evaluate() throws Throwable {
for (int i = 0; i < repeat; i++) {
if (repeat > 1) {
System.out.println(String.format("---> Repeating test [%s:%s], run count [%d]",
testMethod.getDeclaringClass().getCanonicalName(),
testMethod.getName(), i + 1));
}
statement.evaluate();
}
}
}
}