/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.ignite.testframework;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.testframework.junits.GridAbstractTest;
import org.apache.ignite.testsuites.IgniteIgnore;
import org.jetbrains.annotations.Nullable;
import org.junit.internal.MethodSorter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Base class for run junit tests.
* Test methods marked with @Ignored annotation won't be executed.
*/
public class IgniteTestSuite extends TestSuite {
/** Ignore default flag thread local. */
private static final ThreadLocal<Boolean> IGNORE_DFLT = new ThreadLocal<Boolean>() {
@Override protected Boolean initialValue() {
return false;
}
};
/** Whether to execute only ignored tests. */
private boolean ignoredOnly;
/**
* Constructor.
*
* @param name Name.
*/
public IgniteTestSuite(String name) {
this(null, name);
}
/**
* Constructor.
*
* @param theClass TestCase class
*/
public IgniteTestSuite(Class<? extends TestCase> theClass) {
this(theClass, ignoreDefault());
}
/**
* Constructor.
*
* @param theClass TestCase class
* @param ignoredOnly Whether to execute only ignored tests.
*/
public IgniteTestSuite(Class<? extends TestCase> theClass, boolean ignoredOnly) {
this(theClass, null, ignoredOnly);
}
/**
* Constructor.
*
* @param theClass TestCase class
* @param name Test suite name.
*/
public IgniteTestSuite(Class<? extends TestCase> theClass, String name) {
this(theClass, name, ignoreDefault());
}
/**
* Constructor.
*
* @param theClass TestCase class
* @param name Test suite name.
* @param ignoredOnly Whether to execute only ignored tests.
*/
public IgniteTestSuite(@Nullable Class<? extends TestCase> theClass, @Nullable String name, boolean ignoredOnly) {
this.ignoredOnly = ignoredOnly;
if (theClass != null)
addTestsFromTestCase(theClass);
if (name != null)
setName(name);
}
/** {@inheritDoc} */
@Override public void addTest(Test test) {
// Ignore empty test suites.
if (test instanceof IgniteTestSuite) {
IgniteTestSuite suite = (IgniteTestSuite)test;
if (suite.testCount() == 0)
return;
}
super.addTest(test);
}
/** {@inheritDoc} */
@Override public void addTestSuite(Class<? extends TestCase> testClass) {
addTest(new IgniteTestSuite(testClass, ignoredOnly));
}
/**
*
* @param theClass TestCase class
*/
private void addTestsFromTestCase(Class<?> theClass) {
setName(theClass.getName());
try {
getTestConstructor(theClass);
}
catch (NoSuchMethodException ignored) {
addTest(warning("Class " + theClass.getName() +
" has no public constructor TestCase(String name) or TestCase()"));
return;
}
if(!Modifier.isPublic(theClass.getModifiers()))
addTest(warning("Class " + theClass.getName() + " is not public"));
else {
IgnoreDescriptor clsIgnore = IgnoreDescriptor.forClass(theClass);
Class superCls = theClass;
int testAdded = 0;
int testSkipped = 0;
LinkedList<Test> addedTests = new LinkedList<>();
for(List<String> names = new ArrayList<>(); Test.class.isAssignableFrom(superCls);
superCls = superCls.getSuperclass()) {
Method[] methods = MethodSorter.getDeclaredMethods(superCls);
for (Method each : methods) {
AddResult res = addTestMethod(each, names, theClass, clsIgnore);
if (res.added()) {
testAdded++;
addedTests.add(res.test());
}
else
testSkipped++;
}
}
if(testAdded == 0 && testSkipped == 0)
addTest(warning("No tests found in " + theClass.getName()));
// Populate tests count.
for (Test test : addedTests) {
if (test instanceof GridAbstractTest) {
GridAbstractTest test0 = (GridAbstractTest)test;
test0.forceTestCount(addedTests.size());
}
}
}
}
/**
* Add test method.
*
* @param m Test method.
* @param names Test name list.
* @param theClass Test class.
* @param clsIgnore Class ignore descriptor (if any).
* @return Result.
*/
private AddResult addTestMethod(Method m, List<String> names, Class<?> theClass,
@Nullable IgnoreDescriptor clsIgnore) {
String name = m.getName();
if (names.contains(name))
return new AddResult(false, null);
if (!isPublicTestMethod(m)) {
if (isTestMethod(m))
addTest(warning("Test method isn't public: " + m.getName() + "(" + theClass.getCanonicalName() + ")"));
return new AddResult(false, null);
}
names.add(name);
IgnoreDescriptor ignore = IgnoreDescriptor.forMethod(theClass, m);
if (ignore == null)
ignore = clsIgnore;
if (ignoredOnly) {
if (ignore != null) {
Test test = createTest(theClass, name);
if (ignore.forceFailure()) {
if (test instanceof GridAbstractTest)
((GridAbstractTest)test).forceFailure(ignore.reason());
else
test = new ForcedFailure(name, ignore.reason());
}
addTest(test);
return new AddResult(true, test);
}
}
else {
if (ignore == null) {
Test test = createTest(theClass, name);
addTest(test);
return new AddResult(true, test);
}
}
return new AddResult(false, null);
}
/**
* Check whether this is a test method.
*
* @param m Method.
* @return {@code True} if this is a test method.
*/
private static boolean isTestMethod(Method m) {
return m.getParameterTypes().length == 0 &&
m.getName().startsWith("test") &&
m.getReturnType().equals(Void.TYPE);
}
/**
* Check whether this is a public test method.
*
* @param m Method.
* @return {@code True} if this is a public test method.
*/
private static boolean isPublicTestMethod(Method m) {
return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
}
/**
* @param val Default value of ignore flag.
*/
public static void ignoreDefault(boolean val) {
IGNORE_DFLT.set(val);
}
/**
* @return Default value of ignore flag.
*/
private static boolean ignoreDefault() {
Boolean res = IGNORE_DFLT.get();
return res != null && res;
}
/**
* Ignore descriptor.
*/
private static class IgnoreDescriptor {
/** Reason. */
private final String reason;
/** Force failure. */
private final boolean forceFailure;
/**
* Get descriptor for class (if any).
*
* @param cls Class.
* @return Descriptor or {@code null}.
*/
@Nullable public static IgnoreDescriptor forClass(Class cls) {
Class cls0 = cls;
while (Test.class.isAssignableFrom(cls0)) {
if (cls0.isAnnotationPresent(IgniteIgnore.class)) {
IgniteIgnore ignore = (IgniteIgnore)cls0.getAnnotation(IgniteIgnore.class);
String reason = ignore.value();
if (F.isEmpty(reason))
throw new IllegalArgumentException("Reason is not set for ignored test [class=" +
cls0.getName() + ']');
return new IgnoreDescriptor(reason, ignore.forceFailure());
}
cls0 = cls0.getSuperclass();
}
return null;
}
/**
* Get descriptor for method (if any).
*
* @param cls Class.
* @param mthd Method.
* @return Descriptor or {@code null}.
*/
@Nullable public static IgnoreDescriptor forMethod(Class cls, Method mthd) {
if (mthd.isAnnotationPresent(IgniteIgnore.class)) {
IgniteIgnore ignore = mthd.getAnnotation(IgniteIgnore.class);
String reason = ignore.value();
if (F.isEmpty(reason))
throw new IllegalArgumentException("Reason is not set for ignored test [class=" +
cls.getName() + ", method=" + mthd.getName() + ']');
return new IgnoreDescriptor(reason, ignore.forceFailure());
}
else
return null;
}
/**
* Constructor.
*
* @param reason Reason.
* @param forceFailure Force failure.
*/
private IgnoreDescriptor(String reason, boolean forceFailure) {
this.reason = reason;
this.forceFailure = forceFailure;
}
/**
* @return Reason.
*/
public String reason() {
return reason;
}
/**
* @return Force failure.
*/
public boolean forceFailure() {
return forceFailure;
}
}
/**
* Test add result.
*/
private static class AddResult {
/** Result. */
private final boolean added;
/** Test */
private final Test test;
/**
* Constructor.
*
* @param added Result.
* @param test Test.
*/
public AddResult(boolean added, Test test) {
this.added = added;
this.test = test;
}
/**
* @return Result.
*/
public boolean added() {
return added;
}
/**
* @return Test.
*/
public Test test() {
return test;
}
}
/**
* Test case simulating failure.
*/
private static class ForcedFailure extends TestCase {
/** Message. */
private final String msg;
/**
* Constructor.
*
* @param name Name.
* @param msg Message.
*/
private ForcedFailure(String name, String msg) {
super(name);
this.msg = msg;
}
/** {@inheritDoc} */
@Override protected void runTest() {
fail("Forced failure: " + msg + " (extend " + GridAbstractTest.class.getSimpleName() +
" for better output).");
}
}
}