blob: d9bf037a853a252795c7d76c0e26a964dde6828b [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.android.cts.core.runner.support;
import android.util.Log;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Map;
/**
* A {@link Runner} that can TestNG tests.
*
* <p>Implementation note: Avoid extending ParentRunner since that also has
* logic to handle BeforeClass/AfterClass and other junit-specific functionality
* that would be invalid for TestNG.</p>
*/
class TestNgRunner extends Runner implements Filterable {
private static final boolean DEBUG = false;
private Description mDescription;
/** Class name for debugging. */
private String mClassName;
/** Don't include the same method names twice. */
private HashSet<String> mMethodSet = new HashSet<>();
/**
* @param testClass the test class to run
*/
TestNgRunner(Class<?> testClass) {
mDescription = generateTestNgDescription(testClass);
mClassName = testClass.getName();
}
// Runner implementation
@Override
public Description getDescription() {
return mDescription;
}
// Runner implementation
@Override
public int testCount() {
if (!descriptionHasChildren(getDescription())) { // Avoid NPE when description is null.
return 0;
}
// We always follow a flat Parent->Leaf hierarchy, so no recursion necessary.
return getDescription().testCount();
}
// Filterable implementation
@Override
public void filter(Filter filter) throws NoTestsRemainException {
mDescription = filterDescription(mDescription, filter);
if (!descriptionHasChildren(getDescription())) { // Avoid NPE when description is null.
if (DEBUG) {
Log.d("TestNgRunner",
"Filtering has removed all tests :( for class " + mClassName);
}
throw new NoTestsRemainException();
}
if (DEBUG) {
Log.d("TestNgRunner",
"Filtering has retained " + testCount() + " tests for class " + mClassName);
}
}
// Filterable implementation
@Override
public void run(RunNotifier notifier) {
if (!descriptionHasChildren(getDescription())) { // Avoid NPE when description is null.
// Nothing to do.
return;
}
for (Description child : getDescription().getChildren()) {
String className = child.getClassName();
String methodName = child.getMethodName();
Class<?> klass;
try {
klass = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
notifier.fireTestStarted(child);
// Avoid looking at all the methods by just using the string method name.
SingleTestNgTestExecutor.Result result = SingleTestNgTestExecutor.execute(klass, methodName);
if (result.hasFailure()) {
// TODO: get the error messages from testng somehow.
notifier.fireTestFailure(new Failure(child, extractException(result.getFailures())));
}
notifier.fireTestFinished(child);
// TODO: Check @Test(enabled=false) and invoke #fireTestIgnored instead.
}
}
private Throwable extractException(Map<String, Throwable> failures) {
if (failures.isEmpty()) {
return new AssertionError();
}
if (failures.size() == 1) {
return failures.values().iterator().next();
}
StringBuilder errorMessage = new StringBuilder("========== Multiple Failures ==========");
for (Map.Entry<String, Throwable> failureEntry : failures.entrySet()) {
errorMessage.append("\n\n=== "). append(failureEntry.getKey()).append(" ===\n");
Throwable throwable = failureEntry.getValue();
errorMessage
.append(throwable.getClass()).append(": ")
.append(throwable.getMessage());
for (StackTraceElement e : throwable.getStackTrace()) {
if (e.getClassName().equals(getClass().getName())) {
break;
}
errorMessage.append("\n at ").append(e);
}
}
errorMessage.append("\n=======================================\n\n");
return new AssertionError(errorMessage.toString());
}
/**
* Recursively (preorder traversal) apply the filter to all the descriptions.
*
* @return null if the filter rejects the whole tree.
*/
private static Description filterDescription(Description desc, Filter filter) {
if (!filter.shouldRun(desc)) { // XX: Does the filter itself do the recursion?
return null;
}
Description newDesc = desc.childlessCopy();
// Return leafs.
if (!descriptionHasChildren(desc)) {
return newDesc;
}
// Filter all subtrees, only copying them if the filter accepts them.
for (Description child : desc.getChildren()) {
Description filteredChild = filterDescription(child, filter);
if (filteredChild != null) {
newDesc.addChild(filteredChild);
}
}
return newDesc;
}
private Description generateTestNgDescription(Class<?> cls) {
// Add the overall class description as the parent.
Description parent = Description.createSuiteDescription(cls);
if (DEBUG) {
Log.d("TestNgRunner", "Generating TestNg Description for class " + cls.getName());
}
// Add each test method as a child.
for (Method m : cls.getDeclaredMethods()) {
// Filter to only 'public void' signatures.
if ((m.getModifiers() & Modifier.PUBLIC) == 0) {
continue;
}
if (!m.getReturnType().equals(Void.TYPE)) {
continue;
}
// Note that TestNG methods may actually have parameters
// (e.g. with @DataProvider) which TestNG will populate itself.
// Add [Class, MethodName] as a Description leaf node.
String name = m.getName();
if (!mMethodSet.add(name)) {
// Overloaded methods have the same name, don't add them twice.
if (DEBUG) {
Log.d("TestNgRunner", "Already added child " + cls.getName() + "#" + name);
}
continue;
}
Description child = Description.createTestDescription(cls, name);
parent.addChild(child);
if (DEBUG) {
Log.d("TestNgRunner", "Add child " + cls.getName() + "#" + name);
}
}
return parent;
}
private static boolean descriptionHasChildren(Description desc) {
// Note: Although "desc.isTest()" is equivalent to "!desc.getChildren().isEmpty()"
// we add the pre-requisite 2 extra null checks to avoid throwing NPEs.
return desc != null && desc.getChildren() != null && !desc.getChildren().isEmpty();
}
}