blob: 626944a90afaa065287b883c8d6b25ce74643730 [file]
/*
* Copyright (C) 2018 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.releaseparser;
import com.android.cts.releaseparser.ReleaseProto.*;
import com.android.tools.smali.dexlib2.AccessFlags;
import com.android.tools.smali.dexlib2.DexFileFactory;
import com.android.tools.smali.dexlib2.Opcodes;
import com.android.tools.smali.dexlib2.iface.Annotation;
import com.android.tools.smali.dexlib2.iface.AnnotationElement;
import com.android.tools.smali.dexlib2.iface.ClassDef;
import com.android.tools.smali.dexlib2.iface.DexFile;
import com.android.tools.smali.dexlib2.iface.Method;
import com.android.tools.smali.dexlib2.iface.value.TypeEncodedValue;
import com.android.tradefed.testtype.IRemoteTest;
import junit.framework.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
class TestSuiteParser {
// JUNIT3 Test suffix
private static final String TEST_TAG = "Test;";
// Some may ends with Tests e.g. cts/tests/tests/accounts/src/android/accounts/cts/AbstractAuthenticatorTests.java
private static final String TESTS_TAG = "Tests;";
private static final String TEST_PREFIX_TAG = "test";
private static final String DEPRECATED_ANNOTATION_TAG = "Ljava/lang/Deprecated;";
private static final String RUN_WITH_ANNOTATION_TAG = "Lorg/junit/runner/RunWith;";
private static final String TEST_ANNOTATION_TAG = "Lorg/junit/Test;";
private static final String SUPPRESS_ANNOTATION_TAG = "/Suppress;";
private static final String ANDROID_JUNIT4_TEST_TAG =
"Landroid/support/test/runner/AndroidJUnit4;";
private static final String PARAMETERIZED_TEST_TAG = "Lorg/junit/runners/Parameterized;";
// configuration option
private static final String NOT_SHARDABLE_TAG = "not-shardable";
// test class option
private static final String RUNTIME_HIT_TAG = "runtime-hint";
// com.android.tradefed.testtype.AndroidJUnitTest option
private static final String PACKAGE_TAG = "package";
// com.android.compatibility.common.tradefed.testtype.JarHostTest option
private static final String JAR_TAG = "jar";
// com.android.tradefed.testtype.GTest option
private static final String NATIVE_TEST_DEVICE_PATH_TAG = "native-test-device-path";
private static final String MODULE_TAG = "module-name";
private static final String TESTCASES_FOLDER_FORMAT = "testcases/%s";
private static final String SUITE_API_INSTALLER_TAG =
"com.android.tradefed.targetprep.suite.SuiteApkInstaller";
private static final String HOST_TEST_CLASS_TAG =
"com.android.compatibility.common.tradefed.testtype.JarHostTest";
// com.android.tradefed.targetprep.suite.SuiteApkInstaller option
private static final String TEST_FILE_NAME_TAG = "test-file-name";
// com.android.compatibility.common.tradefed.targetprep.FilePusher option
private static final String PUSH_TAG = "push";
// test class
private static final String ANDROID_JUNIT_TEST_TAG =
"com.android.tradefed.testtype.AndroidJUnitTest";
private static final String DEQP_TEST_TAG = "com.drawelements.deqp.runner.DeqpTestRunner";
private static final String GTEST_TAG = "com.android.tradefed.testtype.GTest";
private static final String LIBCORE_TEST_TAG = "com.android.compatibility.testtype.LibcoreTest";
private static final String DALVIK_TEST_TAG = "com.android.compatibility.testtype.DalvikTest";
// Target File Extensions
private static final String CONFIG_EXT_TAG = ".config";
private static final String CONFIG_REGEX = ".config$";
private static final String JAR_EXT_TAG = ".jar";
private static final String APK_EXT_TAG = ".apk";
private static final String SO_EXT_TAG = ".so";
// [module].[class]#[method]
public static final String TESTCASE_NAME_FORMAT = "%s.%s#%s";
private final String mFolderPath;
private ReleaseContent mRelContent;
private TestSuite.Builder mTSBuilder;
TestSuiteParser(ReleaseContent relContent, String folder) {
mFolderPath = folder;
mRelContent = relContent;
}
public TestSuite getTestSuite() {
if (mTSBuilder == null) {
mTSBuilder = praseTestSuite();
}
return mTSBuilder.build();
}
private TestSuite.Builder praseTestSuite() {
TestSuite.Builder tsBuilder = TestSuite.newBuilder();
tsBuilder.setName(mRelContent.getName());
tsBuilder.setVersion(mRelContent.getVersion());
tsBuilder.setBuildNumber(mRelContent.getBuildNumber());
// Iterates all file
for (Entry entry : getFileEntriesList(mRelContent)) {
// Only parses test module config files
if (Entry.EntryType.TEST_MODULE_CONFIG == entry.getType()) {
TestModuleConfig config = entry.getTestModuleConfig();
TestSuite.Module.Builder moduleBuilder = praseModule(config);
moduleBuilder.setConfigFile(entry.getRelativePath());
tsBuilder.addModules(moduleBuilder);
}
}
return tsBuilder;
}
private TestSuite.Module.Builder praseModule(TestModuleConfig config) {
TestSuite.Module.Builder moduleBuilder = TestSuite.Module.newBuilder();
// parse test package and class
List<TestModuleConfig.TestClass> testClassesList = config.getTestClassesList();
moduleBuilder.setName(config.getModuleName());
for (TestModuleConfig.TestClass tClass : testClassesList) {
String testClass = tClass.getTestClass();
moduleBuilder.setTestClass(testClass);
switch (testClass) {
case ANDROID_JUNIT_TEST_TAG:
moduleBuilder.setTestType(TestSuite.TestType.ANDROIDJUNIT);
parseAndroidJUnitTest(moduleBuilder, config, tClass.getTestClass());
break;
case HOST_TEST_CLASS_TAG:
moduleBuilder.setTestType(TestSuite.TestType.JAVAHOST);
parseJavaHostTest(moduleBuilder, config, tClass.getTestClass());
break;
default:
//ToDo
moduleBuilder.setTestType(TestSuite.TestType.UNKNOWN);
ApiPackage.Builder pkgBuilder = ApiPackage.newBuilder();
moduleBuilder.addPackages(pkgBuilder);
System.err.printf(
"ToDo Test Type: %s %s\n", tClass.getTestClass(), tClass.getPackage());
}
}
return moduleBuilder;
}
private void parseAndroidJUnitTest(
TestSuite.Module.Builder moduleBuilder, TestModuleConfig config, String tClass) {
// getting apk list from Test Module Configuration
List<TestModuleConfig.TargetPreparer> tPrepList = config.getTargetPreparersList();
for (TestModuleConfig.TargetPreparer tPrep : tPrepList) {
for (Option opt : tPrep.getOptionsList()) {
if (TEST_FILE_NAME_TAG.equalsIgnoreCase(opt.getName())) {
ApiPackage.Builder pkgBuilder = ApiPackage.newBuilder();
String testFileName = opt.getValue();
Entry tEntry = getFileEntry(testFileName);
pkgBuilder.setName(testFileName);
pkgBuilder.setPackageFile(tEntry.getRelativePath());
pkgBuilder.setContentId(tEntry.getContentId());
parseApkTestCase(pkgBuilder, config);
moduleBuilder.addPackages(pkgBuilder);
}
}
}
}
private Entry getFileEntry(String name) {
Entry fEntry = null;
for (Entry et : getFileEntriesList(mRelContent)) {
if (name.equals(et.getName())) {
fEntry = et;
break;
}
}
return fEntry;
}
// Parses test case list from an APK
private void parseApkTestCase(ApiPackage.Builder pkgBuilder, TestModuleConfig config) {
DexFile dexFile = null;
String apkPath = Paths.get(mFolderPath, pkgBuilder.getPackageFile()).toString();
String moduleName = config.getModuleName();
// Loads a Dex file
try {
dexFile = DexFileFactory.loadDexFile(apkPath, Opcodes.getDefault());
// Iterates through all clesses in the Dex file
for (ClassDef classDef : dexFile.getClasses()) {
// adjust the format Lclass/y;
String className = classDef.getType().replace('/', '.');
// remove L...;
if (className.length() > 2) {
className = className.substring(1, className.length() - 1);
}
// Parses test classes
TestClassType cType = chkTestClassType(classDef);
ApiClass.Builder tClassBuilder = ApiClass.newBuilder();
switch (cType) {
case JUNIT3:
tClassBuilder.setTestClassType(cType);
tClassBuilder.setName(className);
// Checks all test method
for (Method method : classDef.getMethods()) {
// Only care about Public
if ((method.getAccessFlags() & AccessFlags.PUBLIC.getValue()) != 0) {
String mName = method.getName();
// Warn current test result accounting does not work well with Supress
if (hasAnnotationSuffix(
method.getAnnotations(), SUPPRESS_ANNOTATION_TAG)) {
System.err.printf("%s#%s with Suppress:\n", className, mName);
} else if (mName.startsWith(TEST_PREFIX_TAG)) {
// Junit3 style test case name starts with test
tClassBuilder.addMethods(
newTestBuilder(
moduleName, className, method.getName()));
} else if (hasAnnotationSuffix(
method.getAnnotations(), TEST_ANNOTATION_TAG)) {
tClassBuilder.addMethods(
newTestBuilder(
moduleName, className, method.getName()));
System.err.printf(
"%s#%s JUNIT3 mixes with %s annotation:\n",
className, mName, TEST_ANNOTATION_TAG);
}
}
}
pkgBuilder.addClasses(tClassBuilder);
break;
case PARAMETERIZED:
// ToDo need to find a way to count Parameterized tests
System.err.printf("To count Parameterized tests: %s\n", className);
tClassBuilder.setTestClassType(cType);
tClassBuilder.setName(className);
for (Method method : classDef.getMethods()) {
// Junit4 style test case annotated with @Test
if (hasAnnotation(method.getAnnotations(), TEST_ANNOTATION_TAG)) {
tClassBuilder.addMethods(
newTestBuilder(moduleName, className, method.getName()));
}
}
pkgBuilder.addClasses(tClassBuilder);
break;
case JUNIT4:
tClassBuilder.setTestClassType(cType);
tClassBuilder.setName(className);
for (Method method : classDef.getMethods()) {
// Junit4 style test case annotated with @Test
if (hasAnnotation(method.getAnnotations(), TEST_ANNOTATION_TAG)) {
tClassBuilder.addMethods(
newTestBuilder(moduleName, className, method.getName()));
}
}
pkgBuilder.addClasses(tClassBuilder);
break;
default:
// Not a known test class
}
}
} catch (IOException | DexFileFactory.DexFileNotFoundException ex) {
System.err.println("Unable to load dex file: " + apkPath);
// ex.printStackTrace();
}
}
private ApiMethod.Builder newTestBuilder(String moduleName, String className, String testName) {
ApiMethod.Builder testBuilder = ApiMethod.newBuilder();
testBuilder.setName(testName);
// Check if it's an known failure
String nfFilter = getKnownFailureFilter(moduleName, className, testName);
if (null != nfFilter) {
testBuilder.setKnownFailureFilter(nfFilter);
}
return testBuilder;
}
private void parseJavaHostTest(
TestSuite.Module.Builder moduleBuilder, TestModuleConfig config, String tClass) {
ApiPackage.Builder pkgBuilder = ApiPackage.newBuilder();
//Assuming there is only one test Jar
String testFileName = config.getTestJars(0);
Entry tEntry = getFileEntry(testFileName);
String jarPath = tEntry.getRelativePath();
pkgBuilder.setName(testFileName);
pkgBuilder.setPackageFile(jarPath);
pkgBuilder.setContentId(tEntry.getContentId());
Collection<Class<?>> classes =
getJarTestClasses(
Paths.get(mFolderPath, jarPath).toFile(),
// Includes [x]-tradefed.jar for classes such as CompatibilityHostTestBase
Paths.get(mFolderPath, mRelContent.getTestSuiteTradefed()).toFile());
for (Class<?> c : classes) {
ApiClass.Builder tClassBuilder = ApiClass.newBuilder();
tClassBuilder.setTestClassType(TestClassType.JAVAHOST);
tClassBuilder.setName(c.getName());
for (java.lang.reflect.Method m : c.getMethods()) {
int mdf = m.getModifiers();
if (Modifier.isPublic(mdf) || Modifier.isProtected(mdf)) {
if (m.getName().startsWith(TEST_PREFIX_TAG)) {
ApiMethod.Builder methodBuilder = ApiMethod.newBuilder();
methodBuilder.setName(m.getName());
// Check if it's an known failure
String nfFilter =
getKnownFailureFilter(
config.getModuleName(), c.getName(), m.getName());
if (null != nfFilter) {
methodBuilder.setKnownFailureFilter(nfFilter);
}
tClassBuilder.addMethods(methodBuilder);
}
}
}
pkgBuilder.addClasses(tClassBuilder);
}
moduleBuilder.addPackages(pkgBuilder);
}
private static boolean hasAnnotation(Set<? extends Annotation> annotations, String tag) {
for (Annotation annotation : annotations) {
if (annotation.getType().equals(tag)) {
return true;
}
}
return false;
}
private static boolean hasAnnotationSuffix(Set<? extends Annotation> annotations, String tag) {
for (Annotation annotation : annotations) {
if (annotation.getType().endsWith(tag)) {
return true;
}
}
return false;
}
private static TestClassType chkTestClassType(ClassDef classDef) {
// Only care about Public Class
if ((classDef.getAccessFlags() & AccessFlags.PUBLIC.getValue()) == 0) {
return TestClassType.UNKNOWN;
}
for (Annotation annotation : classDef.getAnnotations()) {
if (annotation.getType().equals(DEPRECATED_ANNOTATION_TAG)) {
return TestClassType.UNKNOWN;
}
if (annotation.getType().equals(RUN_WITH_ANNOTATION_TAG)) {
for (AnnotationElement annotationEle : annotation.getElements()) {
if ("value".equals(annotationEle.getName())) {
String aValue = ((TypeEncodedValue) annotationEle.getValue()).getValue();
if (ANDROID_JUNIT4_TEST_TAG.equals(aValue)) {
return TestClassType.JUNIT4;
} else if (PARAMETERIZED_TEST_TAG.equals(aValue)) {
return TestClassType.PARAMETERIZED;
}
}
}
System.err.printf("Unknown test class type: %s\n", classDef.getType());
return TestClassType.JUNIT4;
}
}
if (classDef.getType().endsWith(TEST_TAG) || classDef.getType().endsWith(TESTS_TAG)) {
return TestClassType.JUNIT3;
} else {
return TestClassType.UNKNOWN;
}
}
private static boolean isTargetClass(List<String> pkgList, String className) {
boolean found = false;
for (String pkg : pkgList) {
if (className.startsWith(pkg)) {
found = true;
break;
}
}
return found;
}
private static Collection<Class<?>> getJarTestClasses(File jarTestFile, File tfFile)
throws IllegalArgumentException {
List<Class<?>> classes = new ArrayList<>();
try (JarFile jarFile = new JarFile(jarTestFile)) {
Enumeration<JarEntry> e = jarFile.entries();
URL[] urls = {
new URL(String.format("jar:file:%s!/", jarTestFile.getAbsolutePath())),
new URL(String.format("jar:file:%s!/", tfFile.getAbsolutePath()))
};
URLClassLoader cl =
URLClassLoader.newInstance(urls, JarTestFinder.class.getClassLoader());
while (e.hasMoreElements()) {
JarEntry je = e.nextElement();
if (je.isDirectory()
|| !je.getName().endsWith(".class")
|| je.getName().contains("$")
|| je.getName().contains("junit/")) {
continue;
}
String className = getClassName(je.getName());
/*if (!className.endsWith("Test")) {
continue;
}*/
try {
Class<?> cls = cl.loadClass(className);
if (IRemoteTest.class.isAssignableFrom(cls)
|| Test.class.isAssignableFrom(cls)) {
classes.add(cls);
} else if (!Modifier.isAbstract(cls.getModifiers())
&& hasJUnit4Annotation(cls)) {
classes.add(cls);
}
} catch (ClassNotFoundException | Error x) {
System.err.println(
String.format(
"Cannot find test class %s from %s",
className, jarTestFile.getName()));
x.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/** Helper to determine if we are dealing with a Test class with Junit4 annotations. */
protected static boolean hasJUnit4Annotation(Class<?> classObj) {
if (classObj.isAnnotationPresent(SuiteClasses.class)) {
return true;
}
if (classObj.isAnnotationPresent(RunWith.class)) {
return true;
}
/*for (Method m : classObj.getMethods()) {
if (m.isAnnotationPresent(org.junit.Test.class)) {
return true;
}
}*/
return false;
}
private static String getClassName(String name) {
// -6 because of .class
return name.substring(0, name.length() - 6).replace('/', '.');
}
private String getKnownFailureFilter(String tModule, String tClass, String tMethod) {
List<String> knownFailures = mRelContent.getKnownFailuresList();
String tsName = String.format(TESTCASE_NAME_FORMAT, tModule, tClass, tMethod);
for (String kf : knownFailures) {
if (tsName.startsWith(kf)) {
return kf;
}
}
return null;
}
// Iterates though all test suite content and prints them.
public void writeCsvFile(String relNameVer, String csvFile) {
TestSuite ts = getTestSuite();
try {
FileWriter fWriter = new FileWriter(csvFile);
PrintWriter pWriter = new PrintWriter(fWriter);
//Header
pWriter.println(
"release,module,test_class,test,test_package,test_type,known_failure_filter,package_content_id");
for (TestSuite.Module module : ts.getModulesList()) {
for (ApiPackage pkg : module.getPackagesList()) {
for (ApiClass cls : pkg.getClassesList()) {
for (ApiMethod mtd : cls.getMethodsList()) {
pWriter.printf(
"%s,%s,%s,%s,%s,%s,%s,%s\n",
relNameVer,
module.getName(),
cls.getName(),
mtd.getName(),
pkg.getPackageFile(),
cls.getTestClassType(),
mtd.getKnownFailureFilter(),
getTestTargetContentId(pkg.getPackageFile()));
}
}
}
}
pWriter.flush();
pWriter.close();
} catch (IOException e) {
System.err.println("IOException:" + e.getMessage());
}
}
// Iterates though all test module and prints them.
public void writeModuleCsvFile(String relNameVer, String csvFile) {
TestSuite ts = getTestSuite();
try {
FileWriter fWriter = new FileWriter(csvFile);
PrintWriter pWriter = new PrintWriter(fWriter);
//Header
pWriter.print(
"release,module,test_no,known_failure_no,test_type,test_class,component,description,test_config_file,test_file_names,test_jars,module_content_id\n");
for (TestSuite.Module module : ts.getModulesList()) {
int classCnt = 0;
int methodCnt = 0;
int kfCnt = 0;
for (ApiPackage pkg : module.getPackagesList()) {
for (ApiClass cls : pkg.getClassesList()) {
for (ApiMethod mtd : cls.getMethodsList()) {
// Filter out known failures
if (mtd.getKnownFailureFilter().isEmpty()) {
methodCnt++;
} else {
kfCnt++;
}
}
classCnt++;
}
}
String config = module.getConfigFile();
Entry entry = mRelContent.getEntries().get(config);
TestModuleConfig tmConfig = entry.getTestModuleConfig();
pWriter.printf(
"%s,%s,%d,%d,%s,%s,%s,%s,%s,%s,%s,%s\n",
relNameVer,
module.getName(),
methodCnt,
kfCnt,
module.getTestType(),
module.getTestClass(),
tmConfig.getComponent(),
tmConfig.getDescription(),
config,
String.join(" ", tmConfig.getTestFileNamesList()),
String.join(" ", tmConfig.getTestJarsList()),
getTestModuleContentId(
entry,
tmConfig.getTestFileNamesList(),
tmConfig.getTestJarsList()));
}
pWriter.flush();
pWriter.close();
} catch (IOException e) {
System.err.println("IOException:" + e.getMessage());
}
}
public static Collection<Entry> getFileEntriesList(ReleaseContent relContent) {
return relContent.getEntries().values();
}
// get Test Module Content Id = config cid + apk cids + jar cids
private String getTestModuleContentId(Entry config, List<String> apks, List<String> jars) {
String id = null;
//Starts with config file content_id
String idStr = config.getContentId();
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
//Add all apk content_id
for (String apk : apks) {
idStr += getTestTargetContentId(String.format(TESTCASES_FOLDER_FORMAT, apk));
}
//Add all jar content_id
for (String jar : jars) {
idStr += getTestTargetContentId(String.format(TESTCASES_FOLDER_FORMAT, jar));
}
md.update(idStr.getBytes(StandardCharsets.UTF_8));
// Converts to Base64 String
id = Base64.getEncoder().encodeToString(md.digest());
// System.out.println("getTestModuleContentId: " + idStr);
} catch (NoSuchAlgorithmException e) {
System.err.println("NoSuchAlgorithmException:" + e.getMessage());
}
return id;
}
private String getTestTargetContentId(String targetFile) {
Entry entry = mRelContent.getEntries().get(targetFile);
if (entry != null) {
return entry.getContentId();
} else {
System.err.println("No getTestTargetContentId: " + targetFile);
return "";
}
}
}