mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-06-26 13:24:49 +02:00
This commit adds - SmackReactor / NIO - a framework for finite state machine connections - support for Java 8 - pretty printed XML debug output It also - reworks the integration test framework - raises the minimum Android API level to 19 - introduces XmppNioTcpConnection Furthermore fixes SMACK-801 (at least partly). Java 8 language features are available, but not all runtime library methods. For that we would need to raise the Android API level to 24 or higher.
789 lines
37 KiB
Java
789 lines
37 KiB
Java
/**
|
|
*
|
|
* Copyright 2015-2019 Florian Schmaus
|
|
*
|
|
* 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 org.igniterealtime.smack.inttest;
|
|
|
|
import static org.reflections.ReflectionUtils.getAllMethods;
|
|
import static org.reflections.ReflectionUtils.withAnnotation;
|
|
import static org.reflections.ReflectionUtils.withModifier;
|
|
import static org.reflections.ReflectionUtils.withParametersCount;
|
|
import static org.reflections.ReflectionUtils.withReturnType;
|
|
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.lang.reflect.ParameterizedType;
|
|
import java.lang.reflect.Type;
|
|
import java.security.KeyManagementException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import org.jivesoftware.smack.AbstractXMPPConnection;
|
|
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
|
|
import org.jivesoftware.smack.SmackConfiguration;
|
|
import org.jivesoftware.smack.SmackException;
|
|
import org.jivesoftware.smack.SmackException.NoResponseException;
|
|
import org.jivesoftware.smack.XMPPException;
|
|
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
|
|
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
|
|
import org.jivesoftware.smackx.debugger.EnhancedDebuggerWindow;
|
|
import org.jivesoftware.smackx.iqregister.AccountManager;
|
|
|
|
import org.igniterealtime.smack.inttest.Configuration.AccountRegistration;
|
|
import org.junit.AfterClass;
|
|
import org.junit.BeforeClass;
|
|
import org.reflections.Reflections;
|
|
import org.reflections.scanners.MethodAnnotationsScanner;
|
|
import org.reflections.scanners.MethodParameterScanner;
|
|
import org.reflections.scanners.SubTypesScanner;
|
|
import org.reflections.scanners.TypeAnnotationsScanner;
|
|
|
|
public class SmackIntegrationTestFramework<DC extends AbstractXMPPConnection> {
|
|
|
|
private static final Logger LOGGER = Logger.getLogger(SmackIntegrationTestFramework.class.getName());
|
|
|
|
public static boolean SINTTEST_UNIT_TEST = false;
|
|
|
|
private final Class<DC> defaultConnectionClass;
|
|
|
|
protected final Configuration config;
|
|
|
|
protected TestRunResult testRunResult;
|
|
|
|
private SmackIntegrationTestEnvironment<DC> environment;
|
|
protected XmppConnectionManager<DC> connectionManager;
|
|
|
|
public enum TestType {
|
|
Normal,
|
|
LowLevel,
|
|
SpecificLowLevel,
|
|
}
|
|
|
|
public static void main(String[] args) throws IOException, KeyManagementException,
|
|
NoSuchAlgorithmException, SmackException, XMPPException, InterruptedException, InstantiationException,
|
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
|
Configuration config = Configuration.newConfiguration(args);
|
|
|
|
SmackIntegrationTestFramework<XMPPTCPConnection> sinttest = new SmackIntegrationTestFramework<>(config, XMPPTCPConnection.class);
|
|
TestRunResult testRunResult = sinttest.run();
|
|
|
|
for (Entry<Class<? extends AbstractSmackIntTest>, Throwable> entry : testRunResult.impossibleTestClasses.entrySet()) {
|
|
LOGGER.info("Could not run " + entry.getKey().getName() + " because: "
|
|
+ entry.getValue().getLocalizedMessage());
|
|
}
|
|
for (TestNotPossible testNotPossible : testRunResult.impossibleIntegrationTests) {
|
|
LOGGER.info("Could not run " + testNotPossible.concreteTest + " because: "
|
|
+ testNotPossible.testNotPossibleException.getMessage());
|
|
}
|
|
for (SuccessfulTest successfulTest : testRunResult.successfulIntegrationTests) {
|
|
LOGGER.info(successfulTest.concreteTest + " ✔");
|
|
}
|
|
final int successfulTests = testRunResult.successfulIntegrationTests.size();
|
|
final int failedTests = testRunResult.failedIntegrationTests.size();
|
|
final int totalIntegrationTests = successfulTests + failedTests;
|
|
final int availableTests = testRunResult.getNumberOfAvailableTests();
|
|
final int possibleTests = testRunResult.getNumberOfPossibleTests();
|
|
LOGGER.info("SmackIntegrationTestFramework[" + testRunResult.testRunId + ']' + ": Finished ["
|
|
+ successfulTests + '/' + totalIntegrationTests + "] (" + possibleTests + " test methods of " + availableTests + " where possible)");
|
|
|
|
final int exitStatus;
|
|
if (failedTests > 0) {
|
|
LOGGER.warning("💀 The following " + failedTests + " tests failed! 💀");
|
|
for (FailedTest failedTest : testRunResult.failedIntegrationTests) {
|
|
final Throwable cause = failedTest.failureReason;
|
|
LOGGER.log(Level.SEVERE, failedTest.concreteTest + " failed: " + cause, cause);
|
|
}
|
|
exitStatus = 2;
|
|
} else {
|
|
LOGGER.info("All possible Smack Integration Tests completed successfully. \\o/");
|
|
exitStatus = 0;
|
|
}
|
|
|
|
switch (config.debugger) {
|
|
case enhanced:
|
|
EnhancedDebuggerWindow.getInstance().waitUntilClosed();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
System.exit(exitStatus);
|
|
}
|
|
|
|
public SmackIntegrationTestFramework(Configuration configuration, Class<DC> defaultConnectionClass)
|
|
throws KeyManagementException, InstantiationException, IllegalAccessException, IllegalArgumentException,
|
|
InvocationTargetException, NoSuchAlgorithmException, SmackException, IOException, XMPPException,
|
|
InterruptedException {
|
|
this.config = configuration;
|
|
this.defaultConnectionClass = defaultConnectionClass;
|
|
}
|
|
|
|
public synchronized TestRunResult run()
|
|
throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, XMPPException,
|
|
InterruptedException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
|
testRunResult = new TestRunResult();
|
|
|
|
// Create a connection manager *after* we created the testRunId (in testRunResult).
|
|
this.connectionManager = new XmppConnectionManager<>(this, defaultConnectionClass);
|
|
|
|
LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId + ']' + ": Starting");
|
|
if (config.debugger != Configuration.Debugger.none) {
|
|
// JUL Debugger will not print any information until configured to print log messages of
|
|
// level FINE
|
|
// TODO configure JUL for log?
|
|
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smack.debugger.JulDebugger");
|
|
SmackConfiguration.DEBUG = true;
|
|
}
|
|
if (config.replyTimeout > 0) {
|
|
SmackConfiguration.setDefaultReplyTimeout(config.replyTimeout);
|
|
}
|
|
if (config.securityMode != SecurityMode.required && config.accountRegistration == AccountRegistration.inBandRegistration) {
|
|
AccountManager.sensitiveOperationOverInsecureConnectionDefault(true);
|
|
}
|
|
// TODO print effective configuration
|
|
|
|
String[] testPackages;
|
|
if (config.testPackages == null) {
|
|
testPackages = new String[] { "org.jivesoftware.smackx", "org.jivesoftware.smack" };
|
|
}
|
|
else {
|
|
testPackages = config.testPackages.toArray(new String[config.testPackages.size()]);
|
|
}
|
|
Reflections reflections = new Reflections(testPackages, new SubTypesScanner(),
|
|
new TypeAnnotationsScanner(), new MethodAnnotationsScanner(), new MethodParameterScanner());
|
|
Set<Class<? extends AbstractSmackIntegrationTest>> inttestClasses = reflections.getSubTypesOf(AbstractSmackIntegrationTest.class);
|
|
Set<Class<? extends AbstractSmackLowLevelIntegrationTest>> lowLevelInttestClasses = reflections.getSubTypesOf(AbstractSmackLowLevelIntegrationTest.class);
|
|
|
|
Set<Class<? extends AbstractSmackIntTest>> classes = new HashSet<>(inttestClasses.size()
|
|
+ lowLevelInttestClasses.size());
|
|
classes.addAll(inttestClasses);
|
|
classes.addAll(lowLevelInttestClasses);
|
|
|
|
{
|
|
// Remove all abstract classes.
|
|
// TODO: This may be a good candidate for Java stream filtering once Smack is Android API 24 or higher.
|
|
Iterator<Class<? extends AbstractSmackIntTest>> it = classes.iterator();
|
|
while (it.hasNext()) {
|
|
Class<? extends AbstractSmackIntTest> clazz = it.next();
|
|
if (Modifier.isAbstract(clazz.getModifiers())) {
|
|
it.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (classes.isEmpty()) {
|
|
throw new IllegalStateException("No test classes found");
|
|
}
|
|
|
|
LOGGER.info("SmackIntegrationTestFramework [" + testRunResult.testRunId
|
|
+ "]: Finished scanning for tests, preparing environment");
|
|
environment = prepareEnvironment();
|
|
|
|
try {
|
|
runTests(classes);
|
|
}
|
|
finally {
|
|
// Ensure that the accounts are deleted and disconnected before we continue
|
|
connectionManager.disconnectAndCleanup();
|
|
}
|
|
|
|
return testRunResult;
|
|
}
|
|
|
|
@SuppressWarnings({"unchecked", "Finally"})
|
|
private void runTests(Set<Class<? extends AbstractSmackIntTest>> classes)
|
|
throws InterruptedException, InstantiationException, IllegalAccessException,
|
|
IllegalArgumentException, SmackException, IOException, XMPPException {
|
|
for (Class<? extends AbstractSmackIntTest> testClass : classes) {
|
|
final String testClassName = testClass.getName();
|
|
|
|
// TODO: Move the whole "skipping section" below one layer up?
|
|
|
|
// Skip pseudo integration tests from src/test
|
|
// Although Smack's gradle build files do not state that the 'main' sources classpath also contains the
|
|
// 'test' classes. Some IDEs like Eclipse include them. As result, a real integration test run encounters
|
|
// pseudo integration tests like the DummySmackIntegrationTest which always throws from src/test.
|
|
// It is unclear why this apparently does not happen in the 4.3 branch, one likely cause is
|
|
// compile project(path: ":smack-omemo", configuration: "testRuntime")
|
|
// in
|
|
// smack-integration-test/build.gradle:17
|
|
// added after 4.3 was branched out with
|
|
// 1f731f6318785a84b9741280d586a61dc37ecb2e
|
|
// Now "gradle integrationTest" appear to be never affected by this, i.e., they are executed with the
|
|
// correct classpath. Plain Eclipse, i.e. Smack imported into Eclipse after "gradle eclipse", appear
|
|
// to include *all* classes. Which means those runs sooner or later try to execute
|
|
// DummySmackIntegrationTest. Eclipse with buildship, the gradle plugin for Eclipse, always excludes
|
|
// *all* src/test classes, which means they do not encounter DummySmackIntegrationTest, but this means
|
|
// that the "compile project(path: ":smack-omemo", configuration: "testRuntime")" is not respected,
|
|
// which leads to
|
|
// Exception in thread "main" java.lang.NoClassDefFoundError: org/jivesoftware/smack/test/util/FileTestUtil
|
|
// at org.jivesoftware.smackx.ox.OXSecretKeyBackupIntegrationTest.<clinit>(OXSecretKeyBackupIntegrationTest.java:66)
|
|
// See
|
|
// - https://github.com/eclipse/buildship/issues/354 (Remove test dependencies from runtime classpath)
|
|
// - https://bugs.eclipse.org/bugs/show_bug.cgi?id=482315 (Runtime classpath includes test dependencies)
|
|
// - https://discuss.gradle.org/t/main-vs-test-compile-vs-runtime-classpaths-in-eclipse-once-and-for-all-how/17403
|
|
// - https://bugs.eclipse.org/bugs/show_bug.cgi?id=376616 (Scope of dependencies has no effect on Eclipse compilation)
|
|
if (!SINTTEST_UNIT_TEST && testClassName.startsWith("org.igniterealtime.smack.inttest.unittest")) {
|
|
LOGGER.finer("Skipping integration test '" + testClassName + "' from src/test classpath");
|
|
continue;
|
|
}
|
|
|
|
if (config.enabledTests != null && !isInSet(testClass, config.enabledTests)) {
|
|
LOGGER.info("Skipping test class " + testClassName + " because it is not enabled");
|
|
continue;
|
|
}
|
|
|
|
if (isInSet(testClass, config.disabledTests)) {
|
|
LOGGER.info("Skipping test class " + testClassName + " because it is disalbed");
|
|
continue;
|
|
}
|
|
|
|
final Constructor<? extends AbstractSmackIntTest> cons;
|
|
try {
|
|
cons = testClass.getConstructor(SmackIntegrationTestEnvironment.class);
|
|
}
|
|
catch (NoSuchMethodException | SecurityException e) {
|
|
throw new IllegalArgumentException(
|
|
"Smack Integration Test class does not declare the correct constructor. Is a public Constructor(SmackIntegrationTestEnvironment) missing?",
|
|
e);
|
|
}
|
|
|
|
final List<Method> smackIntegrationTestMethods;
|
|
{
|
|
Method[] testClassMethods = testClass.getMethods();
|
|
smackIntegrationTestMethods = new ArrayList<>(testClassMethods.length);
|
|
for (Method method : testClassMethods) {
|
|
if (!method.isAnnotationPresent(SmackIntegrationTest.class)) {
|
|
continue;
|
|
}
|
|
smackIntegrationTestMethods.add(method);
|
|
}
|
|
}
|
|
|
|
if (smackIntegrationTestMethods.isEmpty()) {
|
|
LOGGER.warning("No Smack integration test methods found in " + testClass);
|
|
continue;
|
|
}
|
|
|
|
testRunResult.numberOfAvailableTestMethods.addAndGet(smackIntegrationTestMethods.size());
|
|
|
|
final AbstractSmackIntTest test;
|
|
try {
|
|
test = cons.newInstance(environment);
|
|
}
|
|
catch (InvocationTargetException e) {
|
|
Throwable cause = e.getCause();
|
|
|
|
throwFatalException(cause);
|
|
|
|
testRunResult.impossibleTestClasses.put(testClass, cause);
|
|
continue;
|
|
}
|
|
|
|
Class<? extends AbstractXMPPConnection> specificLowLevelConnectionClass = null;
|
|
final TestType testType;
|
|
if (test instanceof AbstractSmackSpecificLowLevelIntegrationTest) {
|
|
AbstractSmackSpecificLowLevelIntegrationTest<?> specificLowLevelTest = (AbstractSmackSpecificLowLevelIntegrationTest<?>) test;
|
|
specificLowLevelConnectionClass = specificLowLevelTest.getConnectionClass();
|
|
testType = TestType.SpecificLowLevel;
|
|
} else if (test instanceof AbstractSmackLowLevelIntegrationTest) {
|
|
testType = TestType.LowLevel;
|
|
} else if (test instanceof AbstractSmackIntegrationTest) {
|
|
testType = TestType.Normal;
|
|
} else {
|
|
throw new AssertionError();
|
|
}
|
|
|
|
// Verify the method signatures, throw in case a signature is incorrect.
|
|
for (Method method : smackIntegrationTestMethods) {
|
|
Class<?> retClass = method.getReturnType();
|
|
if (!retClass.equals(Void.TYPE)) {
|
|
throw new IllegalStateException(
|
|
"SmackIntegrationTest annotation on" + method + " that does not return void");
|
|
}
|
|
switch (testType) {
|
|
case Normal:
|
|
final Class<?>[] parameterTypes = method.getParameterTypes();
|
|
if (parameterTypes.length > 0) {
|
|
throw new IllegalStateException(
|
|
"SmackIntegrationTest annotaton on " + method + " that takes arguments ");
|
|
}
|
|
break;
|
|
case LowLevel:
|
|
verifyLowLevelTestMethod(method, AbstractXMPPConnection.class);
|
|
break;
|
|
case SpecificLowLevel:
|
|
verifyLowLevelTestMethod(method, specificLowLevelConnectionClass);
|
|
break;
|
|
}
|
|
}
|
|
|
|
Iterator<Method> it = smackIntegrationTestMethods.iterator();
|
|
while (it.hasNext()) {
|
|
final Method method = it.next();
|
|
final String methodName = method.getName();
|
|
if (config.enabledTests != null && !(config.enabledTests.contains(methodName)
|
|
|| isInSet(testClass, config.enabledTests))) {
|
|
LOGGER.fine("Skipping test method " + methodName + " because it is not enabled");
|
|
it.remove();
|
|
continue;
|
|
}
|
|
if (config.disabledTests != null && config.disabledTests.contains(methodName)) {
|
|
LOGGER.info("Skipping test method " + methodName + " because it is disabled");
|
|
it.remove();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (smackIntegrationTestMethods.isEmpty()) {
|
|
LOGGER.info("All tests in " + testClassName + " are disabled");
|
|
continue;
|
|
}
|
|
|
|
final int detectedTestMethodsCount = smackIntegrationTestMethods.size();
|
|
testRunResult.numberOfPossibleTestMethods.addAndGet(detectedTestMethodsCount);
|
|
|
|
try {
|
|
// Run the @BeforeClass methods (if any)
|
|
Set<Method> beforeClassMethods = getAllMethods(testClass,
|
|
withAnnotation(BeforeClass.class), withReturnType(Void.TYPE),
|
|
withParametersCount(0), withModifier(Modifier.PUBLIC
|
|
));
|
|
|
|
// See if there are any methods that have the @BeforeClassAnnotation but a wrong signature
|
|
Set<Method> allBeforeClassMethods = getAllMethods(testClass, withAnnotation(BeforeClass.class));
|
|
allBeforeClassMethods.removeAll(beforeClassMethods);
|
|
if (!allBeforeClassMethods.isEmpty()) {
|
|
throw new IllegalArgumentException("@BeforeClass methods with wrong signature found");
|
|
}
|
|
|
|
if (beforeClassMethods.size() == 1) {
|
|
Method beforeClassMethod = beforeClassMethods.iterator().next();
|
|
LOGGER.info("Executing @BeforeClass method of " + testClass);
|
|
try {
|
|
beforeClassMethod.invoke(test);
|
|
}
|
|
catch (InvocationTargetException | IllegalAccessException e) {
|
|
LOGGER.log(Level.SEVERE, "Exception executing @BeforeClass method", e);
|
|
}
|
|
catch (IllegalArgumentException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
else if (beforeClassMethods.size() > 1) {
|
|
throw new IllegalArgumentException("Only one @BeforeClass method allowed");
|
|
}
|
|
|
|
for (Method testMethod : smackIntegrationTestMethods) {
|
|
List<ConcreteTest> concreteTests = null;
|
|
switch (testType) {
|
|
case Normal: {
|
|
ConcreteTest.Executor concreteTestExecutor = () -> testMethod.invoke(test);
|
|
ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor);
|
|
concreteTests = Collections.singletonList(concreteTest);
|
|
}
|
|
break;
|
|
case LowLevel:
|
|
case SpecificLowLevel:
|
|
LowLevelTestMethod lowLevelTestMethod = new LowLevelTestMethod(testMethod);
|
|
switch (testType) {
|
|
case LowLevel:
|
|
concreteTests = invokeLowLevel(lowLevelTestMethod, (AbstractSmackLowLevelIntegrationTest) test);
|
|
break;
|
|
case SpecificLowLevel: {
|
|
ConcreteTest.Executor concreteTestExecutor = () -> invokeSpecificLowLevel(
|
|
lowLevelTestMethod, (AbstractSmackSpecificLowLevelIntegrationTest<?>) test);
|
|
ConcreteTest concreteTest = new ConcreteTest(testType, testMethod, concreteTestExecutor);
|
|
concreteTests = Collections.singletonList(concreteTest);
|
|
break;
|
|
}
|
|
default:
|
|
throw new AssertionError();
|
|
}
|
|
break;
|
|
}
|
|
|
|
for (ConcreteTest concreteTest : concreteTests) {
|
|
runConcreteTest(concreteTest);
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
// Run the @AfterClass method (if any)
|
|
Set<Method> afterClassMethods = getAllMethods(testClass,
|
|
withAnnotation(AfterClass.class), withReturnType(Void.TYPE),
|
|
withParametersCount(0), withModifier(Modifier.PUBLIC
|
|
));
|
|
|
|
// See if there are any methods that have the @AfterClassAnnotation but a wrong signature
|
|
Set<Method> allAfterClassMethods = getAllMethods(testClass, withAnnotation(AfterClass.class));
|
|
allAfterClassMethods.removeAll(afterClassMethods);
|
|
if (!allAfterClassMethods.isEmpty()) {
|
|
throw new IllegalArgumentException("@AfterClass methods with wrong signature found");
|
|
}
|
|
|
|
if (afterClassMethods.size() == 1) {
|
|
Method afterClassMethod = afterClassMethods.iterator().next();
|
|
LOGGER.info("Executing @AfterClass method of " + testClass);
|
|
try {
|
|
afterClassMethod.invoke(test);
|
|
}
|
|
catch (InvocationTargetException | IllegalAccessException e) {
|
|
LOGGER.log(Level.SEVERE, "Exception executing @AfterClass method", e);
|
|
}
|
|
catch (IllegalArgumentException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
else if (afterClassMethods.size() > 1) {
|
|
throw new IllegalArgumentException("Only one @AfterClass method allowed");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void runConcreteTest(ConcreteTest concreteTest)
|
|
throws InterruptedException, XMPPException, IOException, SmackException {
|
|
LOGGER.info(concreteTest + " Start");
|
|
long testStart = System.currentTimeMillis();
|
|
try {
|
|
concreteTest.executor.execute();
|
|
long testEnd = System.currentTimeMillis();
|
|
LOGGER.info(concreteTest + " Success");
|
|
testRunResult.successfulIntegrationTests.add(new SuccessfulTest(concreteTest, testStart, testEnd, null));
|
|
}
|
|
catch (InvocationTargetException e) {
|
|
long testEnd = System.currentTimeMillis();
|
|
Throwable cause = e.getCause();
|
|
if (cause instanceof TestNotPossibleException) {
|
|
LOGGER.info(concreteTest + " is not possible");
|
|
testRunResult.impossibleIntegrationTests.add(new TestNotPossible(concreteTest, testStart, testEnd,
|
|
null, (TestNotPossibleException) cause));
|
|
return;
|
|
}
|
|
Throwable nonFatalFailureReason;
|
|
// junit assert's throw an AssertionError if they fail, those should not be
|
|
// thrown up, as it would be done by throwFatalException()
|
|
if (cause instanceof AssertionError) {
|
|
nonFatalFailureReason = cause;
|
|
} else {
|
|
nonFatalFailureReason = throwFatalException(cause);
|
|
}
|
|
// An integration test failed
|
|
testRunResult.failedIntegrationTests.add(new FailedTest(concreteTest, testStart, testEnd, null,
|
|
nonFatalFailureReason));
|
|
LOGGER.log(Level.SEVERE, concreteTest + " Failed", e);
|
|
}
|
|
catch (IllegalArgumentException | IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
private static void verifyLowLevelTestMethod(Method method,
|
|
Class<? extends AbstractXMPPConnection> connectionClass) {
|
|
if (!testMethodParametersIsListOfConnections(method, connectionClass)
|
|
&& !testMethodParametersVarargsConnections(method, connectionClass)) {
|
|
throw new IllegalArgumentException(method + " is not a valid low level test method");
|
|
}
|
|
}
|
|
|
|
private List<ConcreteTest> invokeLowLevel(LowLevelTestMethod lowLevelTestMethod, AbstractSmackLowLevelIntegrationTest test) {
|
|
Set<Class<? extends AbstractXMPPConnection>> connectionClasses;
|
|
if (lowLevelTestMethod.smackIntegrationTestAnnotation.onlyDefaultConnectionType()) {
|
|
Class<? extends AbstractXMPPConnection> defaultConnectionClass = connectionManager.getDefaultConnectionClass();
|
|
connectionClasses = Collections.singleton(defaultConnectionClass);
|
|
} else {
|
|
connectionClasses = connectionManager.getConnectionClasses();
|
|
}
|
|
|
|
List<ConcreteTest> resultingConcreteTests = new ArrayList<>(connectionClasses.size());
|
|
|
|
for (Class<? extends AbstractXMPPConnection> connectionClass : connectionClasses) {
|
|
ConcreteTest.Executor executor = () -> lowLevelTestMethod.invoke(test, connectionClass);
|
|
ConcreteTest concreteTest = new ConcreteTest(TestType.LowLevel, lowLevelTestMethod.testMethod, executor, connectionClass.getSimpleName());
|
|
resultingConcreteTests.add(concreteTest);
|
|
}
|
|
|
|
return resultingConcreteTests;
|
|
}
|
|
|
|
private <C extends AbstractXMPPConnection> void invokeSpecificLowLevel(LowLevelTestMethod testMethod,
|
|
AbstractSmackSpecificLowLevelIntegrationTest<C> test)
|
|
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InterruptedException,
|
|
SmackException, IOException, XMPPException {
|
|
if (testMethod.smackIntegrationTestAnnotation.onlyDefaultConnectionType()) {
|
|
throw new IllegalArgumentException("SpecificLowLevelTests must not have set onlyDefaultConnectionType");
|
|
}
|
|
Class<C> connectionClass = test.getConnectionClass();
|
|
testMethod.invoke(test, connectionClass);
|
|
}
|
|
|
|
protected SmackIntegrationTestEnvironment<DC> prepareEnvironment() throws SmackException,
|
|
IOException, XMPPException, InterruptedException, KeyManagementException,
|
|
NoSuchAlgorithmException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
|
return connectionManager.prepareEnvironment();
|
|
}
|
|
|
|
enum AccountNum {
|
|
One,
|
|
Two,
|
|
Three,
|
|
}
|
|
|
|
static XMPPTCPConnectionConfiguration.Builder getConnectionConfigurationBuilder(Configuration config) {
|
|
XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder();
|
|
|
|
config.configurationApplier.applyConfigurationTo(builder);
|
|
|
|
return builder;
|
|
}
|
|
|
|
private static Exception throwFatalException(Throwable e) throws Error, NoResponseException,
|
|
InterruptedException {
|
|
if (e instanceof NoResponseException) {
|
|
throw (NoResponseException) e;
|
|
}
|
|
if (e instanceof InterruptedException) {
|
|
throw (InterruptedException) e;
|
|
}
|
|
if (e instanceof RuntimeException) {
|
|
throw (RuntimeException) e;
|
|
}
|
|
if (e instanceof Error) {
|
|
throw (Error) e;
|
|
}
|
|
return (Exception) e;
|
|
}
|
|
|
|
private static boolean isInSet(Class<?> clz, Set<String> classes) {
|
|
if (classes == null) {
|
|
return false;
|
|
}
|
|
final String className = clz.getName();
|
|
final String unqualifiedClassName = clz.getSimpleName();
|
|
return (classes.contains(className) || classes.contains(unqualifiedClassName));
|
|
}
|
|
|
|
public static final class TestRunResult {
|
|
|
|
/**
|
|
* A short String of lowercase characters and numbers used to identify a integration test
|
|
* run. We use lowercase characters because this string will eventually be part of the
|
|
* localpart of the used JIDs (and the localpart is case insensitive).
|
|
*/
|
|
public final String testRunId = StringUtils.insecureRandomString(5).toLowerCase(Locale.US);
|
|
|
|
private final List<SuccessfulTest> successfulIntegrationTests = Collections.synchronizedList(new LinkedList<SuccessfulTest>());
|
|
private final List<FailedTest> failedIntegrationTests = Collections.synchronizedList(new LinkedList<FailedTest>());
|
|
private final List<TestNotPossible> impossibleIntegrationTests = Collections.synchronizedList(new LinkedList<TestNotPossible>());
|
|
private final Map<Class<? extends AbstractSmackIntTest>, Throwable> impossibleTestClasses = new HashMap<>();
|
|
private final AtomicInteger numberOfAvailableTestMethods = new AtomicInteger();
|
|
private final AtomicInteger numberOfPossibleTestMethods = new AtomicInteger();
|
|
|
|
TestRunResult() {
|
|
}
|
|
|
|
public String getTestRunId() {
|
|
return testRunId;
|
|
}
|
|
|
|
public int getNumberOfAvailableTests() {
|
|
return numberOfAvailableTestMethods.get();
|
|
}
|
|
|
|
public int getNumberOfPossibleTests() {
|
|
return numberOfPossibleTestMethods.get();
|
|
}
|
|
|
|
public List<SuccessfulTest> getSuccessfulTests() {
|
|
return Collections.unmodifiableList(successfulIntegrationTests);
|
|
}
|
|
|
|
public List<FailedTest> getFailedTests() {
|
|
return Collections.unmodifiableList(failedIntegrationTests);
|
|
}
|
|
|
|
public List<TestNotPossible> getNotPossibleTests() {
|
|
return Collections.unmodifiableList(impossibleIntegrationTests);
|
|
}
|
|
|
|
public Map<Class<? extends AbstractSmackIntTest>, Throwable> getImpossibleTestClasses() {
|
|
return Collections.unmodifiableMap(impossibleTestClasses);
|
|
}
|
|
}
|
|
|
|
static final class ConcreteTest {
|
|
private final TestType testType;
|
|
private final Method method;
|
|
private final Executor executor;
|
|
private final String[] subdescriptons;
|
|
|
|
private ConcreteTest(TestType testType, Method method, Executor executor, String... subdescriptions) {
|
|
this.testType = testType;
|
|
this.method = method;
|
|
this.executor = executor;
|
|
this.subdescriptons = subdescriptions;
|
|
}
|
|
|
|
private transient String stringCache;
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (stringCache != null) {
|
|
return stringCache;
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append(method.getDeclaringClass().getSimpleName())
|
|
.append('.')
|
|
.append(method.getName())
|
|
.append(" (")
|
|
.append(testType.name());
|
|
final String SUBDESCRIPTION_DELIMITER = ", ";
|
|
sb.append(SUBDESCRIPTION_DELIMITER);
|
|
|
|
for (String subdescripton : subdescriptons) {
|
|
sb.append(subdescripton).append(SUBDESCRIPTION_DELIMITER);
|
|
}
|
|
sb.setLength(sb.length() - SUBDESCRIPTION_DELIMITER.length());
|
|
sb.append(')');
|
|
|
|
stringCache = sb.toString();
|
|
return stringCache;
|
|
}
|
|
|
|
private interface Executor {
|
|
|
|
/**
|
|
* Execute the test.
|
|
*
|
|
* @throws IllegalAccessException
|
|
* @throws InterruptedException
|
|
* @throws InvocationTargetException if the reflective invoked test throws an exception.
|
|
* @throws XMPPException in case an XMPPException happens when <em>preparing</em> the test.
|
|
* @throws IOException in case an IOException happens when <em>preparing</em> the test.
|
|
* @throws SmackException in case an SmackException happens when <em>preparing</em> the test.
|
|
*/
|
|
void execute() throws IllegalAccessException, InterruptedException, InvocationTargetException,
|
|
XMPPException, IOException, SmackException;
|
|
}
|
|
}
|
|
|
|
private final class LowLevelTestMethod {
|
|
private final Method testMethod;
|
|
private final SmackIntegrationTest smackIntegrationTestAnnotation;
|
|
private final boolean parameterListOfConnections;
|
|
|
|
private LowLevelTestMethod(Method testMethod) {
|
|
this.testMethod = testMethod;
|
|
|
|
smackIntegrationTestAnnotation = testMethod.getAnnotation(SmackIntegrationTest.class);
|
|
assert (smackIntegrationTestAnnotation != null);
|
|
parameterListOfConnections = testMethodParametersIsListOfConnections(testMethod);
|
|
}
|
|
|
|
private void invoke(AbstractSmackLowLevelIntegrationTest test,
|
|
Class<? extends AbstractXMPPConnection> connectionClass)
|
|
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
|
|
InterruptedException, SmackException, IOException, XMPPException {
|
|
final int connectionCount;
|
|
if (parameterListOfConnections) {
|
|
connectionCount = smackIntegrationTestAnnotation.connectionCount();
|
|
if (connectionCount < 1) {
|
|
throw new IllegalArgumentException(testMethod + " is annotated to use less than one connection ('"
|
|
+ connectionCount + ')');
|
|
}
|
|
} else {
|
|
connectionCount = testMethod.getParameterCount();
|
|
}
|
|
|
|
List<? extends AbstractXMPPConnection> connections = connectionManager.constructConnectedConnections(
|
|
connectionClass, connectionCount);
|
|
|
|
if (parameterListOfConnections) {
|
|
testMethod.invoke(test, connections);
|
|
} else {
|
|
Object[] connectionsArray = new Object[connectionCount];
|
|
for (int i = 0; i < connectionsArray.length; i++) {
|
|
connectionsArray[i] = connections.remove(0);
|
|
}
|
|
testMethod.invoke(test, connectionsArray);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean testMethodParametersIsListOfConnections(Method testMethod) {
|
|
return testMethodParametersIsListOfConnections(testMethod, AbstractXMPPConnection.class);
|
|
}
|
|
|
|
static boolean testMethodParametersIsListOfConnections(Method testMethod, Class<? extends AbstractXMPPConnection> connectionClass) {
|
|
Type[] parameterTypes = testMethod.getGenericParameterTypes();
|
|
if (parameterTypes.length != 1) {
|
|
return false;
|
|
}
|
|
Class<?> soleParameter = testMethod.getParameterTypes()[0];
|
|
if (!Collection.class.isAssignableFrom(soleParameter)) {
|
|
return false;
|
|
}
|
|
|
|
ParameterizedType soleParameterizedType = (ParameterizedType) parameterTypes[0];
|
|
Type[] actualTypeArguments = soleParameterizedType.getActualTypeArguments();
|
|
if (actualTypeArguments.length != 1) {
|
|
return false;
|
|
}
|
|
|
|
Type soleActualTypeArgument = actualTypeArguments[0];
|
|
if (!(soleActualTypeArgument instanceof Class<?>)) {
|
|
return false;
|
|
}
|
|
Class<?> soleActualTypeArgumentAsClass = (Class<?>) soleActualTypeArgument;
|
|
if (!connectionClass.isAssignableFrom(soleActualTypeArgumentAsClass)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static boolean testMethodParametersVarargsConnections(Method testMethod, Class<? extends AbstractXMPPConnection> connectionClass) {
|
|
Class<?>[] parameterTypes = testMethod.getParameterTypes();
|
|
for (Class<?> parameterType : parameterTypes) {
|
|
if (!parameterType.isAssignableFrom(connectionClass)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|