blob: 57cc3879edef13a05db84b5348802756f729fdf8 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test;
import androidx.test.internal.runner.listener.InstrumentationRunListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runners.model.InitializationError;
import org.chromium.base.Log;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** A RunListener that list out all the test information into a json file. */
public class TestListInstrumentationRunListener extends InstrumentationRunListener {
private static final String TAG = "TestListRunListener";
private static final Set<String> SKIP_METHODS =
new HashSet<>(
Arrays.asList(
new String[] {"toString", "hashCode", "annotationType", "equals"}));
private final Map<Class<?>, JSONObject> mTestClassJsonMap = new HashMap<>();
private Failure mFirstFailure;
@Override
public void testFailure(Failure failure) {
if (mFirstFailure == null) {
mFirstFailure = failure;
}
}
@Override
public void testFinished(Description desc) throws Exception {
Class<?> testClass = desc.getTestClass();
JSONObject classEntry = mTestClassJsonMap.get(testClass);
if (classEntry == null) {
classEntry =
new JSONObject()
.put("class", testClass.getName())
.put("superclass", testClass.getSuperclass().getName())
.put(
"annotations",
getAnnotationJSON(Arrays.asList(testClass.getAnnotations())))
.put("methods", new JSONArray());
mTestClassJsonMap.put(testClass, classEntry);
}
((JSONArray) classEntry.get("methods")).put(getTestMethodJSON(desc));
}
/**
* Store the test method description to a Map at the beginning of a test
* run.
*/
@Override
public void testStarted(Description desc) throws Exception {
// BaseJUnit4ClassRunner only fires testFinished(), so a call to
// testStarted means a different runner is active, and the test is
// actually being executed rather than just listed.
throw new InitializationError(
"All tests must use"
+ " @RunWith(BaseJUnit4ClassRunner.class) or a subclass thereof."
+ " Found that this test does not: "
+ desc.getTestClass());
}
/**
* Create a JSONArray with all the test class JSONObjects and save it to
* listed output path.
*/
public void saveTestsToJson(String outputPath) throws IOException {
if (mFirstFailure != null) {
throw new RuntimeException(
"Failed on " + mFirstFailure.getDescription(), mFirstFailure.getException());
}
try (Writer writer = new OutputStreamWriter(new FileOutputStream(outputPath), "UTF-8")) {
JSONArray allTestClassesJSON = new JSONArray(mTestClassJsonMap.values());
writer.write(allTestClassesJSON.toString());
} catch (IOException e) {
Log.e(TAG, "failed to write json to file", e);
throw e;
}
}
/** Return a JSONObject that represent a Description of a method". */
static JSONObject getTestMethodJSON(Description desc) throws Exception {
return new JSONObject()
.put("method", desc.getMethodName())
.put("annotations", getAnnotationJSON(desc.getAnnotations()));
}
/**
* Make a JSONObject dictionary out of annotations, keyed by the
* Annotation types' simple java names.
*
* For example, for the following group of annotations for ExampleClass
* <code>
* @A
* @B(message = "hello", level = 3)
* public class ExampleClass() {}
* </code>
*
* This method would return a JSONObject as such:
* <code>
* {
* "A": {},
* "B": {
* "message": "hello",
* "level": "3"
* }
* }
* </code>
*
* The method accomplish this by though through each annotation and
* reflectively call the annotation's method to get the element value,
* with exceptions to methods like "equals()" or "hashCode".
*/
static JSONObject getAnnotationJSON(Collection<Annotation> annotations)
throws IllegalAccessException, InvocationTargetException, JSONException {
JSONObject result = new JSONObject();
for (Annotation a : annotations) {
JSONObject aJSON = (JSONObject) asJSON(a);
String aType = aJSON.keys().next();
result.put(aType, aJSON.get(aType));
}
return result;
}
/**
* Recursively serialize an Annotation or an Annotation field value to
* a JSON compatible type.
*/
private static Object asJSON(Object obj)
throws IllegalAccessException, InvocationTargetException, JSONException {
// Use instanceof to determine if it is an Annotation.
// obj.getClass().isAnnotation() doesn't work as expected because
// obj.getClass() returns a proxy class.
if (obj instanceof Annotation) {
Class<? extends Annotation> annotationType = ((Annotation) obj).annotationType();
JSONObject json = new JSONObject();
for (Method method : annotationType.getMethods()) {
if (SKIP_METHODS.contains(method.getName())) {
continue;
}
json.put(method.getName(), asJSON(method.invoke(obj)));
}
JSONObject outerJson = new JSONObject();
// If proguard is enabled and InnerClasses attribute is not kept,
// then getCanonicalName() will return Outer$Inner instead of
// Outer.Inner. So just use getName().
outerJson.put(
annotationType
.getName()
.replaceFirst(annotationType.getPackage().getName() + ".", ""),
json);
return outerJson;
} else {
Class<?> clazz = obj.getClass();
if (clazz.isArray()) {
JSONArray jarr = new JSONArray();
for (int i = 0; i < Array.getLength(obj); i++) {
jarr.put(asJSON(Array.get(obj, i)));
}
return jarr;
} else {
return obj;
}
}
}
}