CTS测试框架 -- V1版本

来源:互联网 发布:开票软件默认密码 编辑:程序博客网 时间:2024/06/07 09:54

目录

  • 概述
  • 组织case
    • CTS框架配置文件
    • 测试case配置文件
    • 启动框架CtsConsole
    • test组件CtsTest
    • 测试类型
  • 执行命令
  • 总结

1 概述

CTS测试框架是有两个版本的,Android 6.0以及之前的版本都统称为V1版本,7.0以及之后的版本为V2(目前Android版本已经迭代到Android O了,目前还是用的V2框架),其实两者都是基于基础框架Trade-Federation进行了封装,定义了case的组织方式,不过两个的解析以及组织方式并不一样。
前面已经介绍过了基础框架,可以在运行时注入动态替换组件,CTS测试框架的封装正是通过这种方式,指定了自己的组件,在组件中定义了自己的处理逻辑,主要包括plan的解析,case的组织,case的分类等,这里先介绍V1版本的处理方式,下篇文章介绍V2版本的处理方式。

2 组织case

开始之前首先说明plan的概念:执行CTS测试是以plan为单位的,一个plan是一组测试的集合,不同的plan代表着执行不同的集合中的测试case。就像cts这个plan,就代表要执行所有的CTS测试case。
另外,无论是plan,还是case,包括运行的脚本,都是Google提供的,厂商需要做的就是连接手机,执行命令运行测试生成报告。

2.1 CTS框架配置文件

文件位置:/cts/tools/tradefed-host/res/config/cts.xml

cts.xml:

<configuration    description="Runs a CTS plan from a pre-existing CTS installation">    <option name="enable-root" value="false" />    <build_provider class="com.android.cts.tradefed.build.CtsBuildProvider" />    <device_recovery class="com.android.tradefed.device.WaitDeviceRecovery" />    <test class="com.android.cts.tradefed.testtype.CtsTest" />    <logger class="com.android.tradefed.log.FileLogger" />    <result_reporter class="com.android.cts.tradefed.result.CtsXmlResultReporter" />    <result_reporter class="com.android.cts.tradefed.result.CtsTestLogReporter" />    <result_reporter class="com.android.cts.tradefed.result.IssueReporter" /></configuration>

这个文件中定义的就是CTS测试自己定义的组件的实现类,也就是说框架的运行流程不变,运行时替换文件中的组件,其中有build_providertestlogger等组件的定义,最重要的还是test组件,因为按照我们前面的分析,其他组件都是为了辅助测试的运行而存在的,而基础框架执行到最后执行的就是预先写好的模板中的setup,run,tearDown方法,这些方法就是test组件中的方法,所以真正执行的真是test组件,也就是CtsTest.java这个类。

2.2 测试case配置文件

按照前面的说法,CTS测试的执行是以plan为单位的,所以既然CtsTest中定义了case的组织,那就有必要先来看看这个plan究竟长什么样子。
注:这个文件是Google提供的测试包中的,由于文件中内容很多,所以这里就列举了一小部分,不过也足以说明了。

<TestPlan version="1.0">  <Entry name="android.JobScheduler"/>  <Entry name="android.aadb"/>  <Entry name="android.acceleration"/>  <Entry name="android.accessibility"/>  <Entry name="android.accessibilityservice"/>  ...</TestPlan>

看起来这个plan文件中好像也没写,就只是列了一堆类似包名的东西。
其实这个地方的Entry中的name正是它要执行的测试case的appPackageName,可以看下面的android.JobScheduler对应的测试case的xml文件:

<?xml version="1.0" encoding="UTF-8"?><TestPackage appNameSpace="android.jobscheduler.cts.deviceside" appPackageName="android.JobScheduler" name="CtsJobSchedulerDeviceTestCases" runner="android.support.test.runner.AndroidJUnitRunner" runtimeHint="0" version="1.0"><TestSuite name="android"><TestSuite name="jobscheduler"><TestSuite name="cts"><TestCase name="ConnectivityConstraintTest"><Test name="testAndroidTestCaseSetupProperly" abis="armeabi-v7a, arm64-v8a" /></TestCase><TestCase name="TimingConstraintsTest"><Test name="testAndroidTestCaseSetupProperly" abis="armeabi-v7a, arm64-v8a" /></TestCase></TestSuite></TestSuite></TestSuite></TestPackage> 

配置文件中的内容更多,这里也只是列了一小部分,说明下结构即可。
最重要的是其中的Test标签,每个标签代表了一条测试。

还有部分测试会有的config文件,这个后面再说,这里先说下结构:

<configuration description="CTS device admin test config">    <include name="common-config" />    <option name="run-command:run-command" value="dpm set-active-admin android.deviceadmin.cts/.CtsDeviceAdminReceiver" />    <option name="run-command:run-command" value="dpm set-active-admin android.deviceadmin.cts/.CtsDeviceAdminReceiver2" /></configuration>

2.3 启动框架CtsConsole

CTS测试框架代码位置: /cts/tools/tradefed-host/src
CtsConsole.java位置: /cts/tools/tradefed-host/src/com/android/cts/tradefed/command/CtsConsole.java

这里从名称上就可以看出来,正是CTS测试的入口,它是基础框架中的Console的子类,有兴趣可以去看下这个文件中的内容,这里就不做罗列了。
其中的内容很很简单,跟Console类中的main一样,这个地方的main创建了一个CtsConsole对象并开启线程,还有一点,因为是自定义,它还复写了父类的setCustomCommands方法,这样就可以添加自己的命令。

2.4 test组件CtsTest

CtsConsole.java位置: /cts/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java

前面介绍了半天的基础,现在终于进入正戏了,CtsTest这个文件正是test组件,我们也可以看下它类的定义:

public class CtsTest implements IDeviceTest, IResumableTest, IShardableTest, IBuildReceiver

可以看出来,它实现了很多的接口,我们直奔它的run方法即可(方法很长,这里只列出重要的方法):

public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {    ...    // 拿到当前设备支持的abi    Set<String> abiSet = getAbis();    ...    // 这个方法虽然看起来只有一行,但是完成了case的组织    setupTestPackageList(abiSet);    ...    // 获取需要在执行测试需要安装的apk    Map<String, Set<String>> prerequisiteApks = getPrerequisiteApks(mTestPackageList, abiSet);    Collection<String> uninstallPackages = getPrerequisitePackageNames(mTestPackageList);    try {        // 这一步是收集设备信息,所有的测试case在执行之前都需要        collectDeviceInfo(getDevice(), mCtsBuild, listener);        prepareReportLogContainers(getDevice(), mBuildInfo);        preRebootIfNecessary(mTestPackageList);        mPrevRebootTime = System.currentTimeMillis();        int remainingPackageCount = mTestPackageList.size();        IAbi currentAbi = null;        // 此时已经拿到了所有的要执行的测试package,遍历        // 执行一些测试之前的预准备        for (int i = mLastTestPackageIndex; i < mTestPackageList.size(); i++) {            TestPackage testPackage = mTestPackageList.get(i);            if (currentAbi == null ||                !currentAbi.getName().equals(testPackage.getAbi().getName())) {                currentAbi = testPackage.getAbi();                installPrerequisiteApks(                    prerequisiteApks.get(currentAbi.getName()), currentAbi);            }            IRemoteTest test = testPackage.getTestForPackage();            // 这里就是从pkg中取出test,看它是哪种接口,就执行相应的接口方法            // 其实就是看测试类型            if (test instanceof IBuildReceiver) {                ((IBuildReceiver) test).setBuild(mBuildInfo);            }            if (test instanceof IDeviceTest) {                ((IDeviceTest) test).setDevice(getDevice());            }            if (test instanceof DeqpTestRunner) {                ((DeqpTestRunner)test).setCollectLogs(mCollectDeqpLogs);            }            if (test instanceof GeeTest) {                if (!mPositiveFilters.isEmpty()) {                    String positivePatterns = join(mPositiveFilters, ":");                    ((GeeTest)test).setPositiveFilters(positivePatterns);                }                if (!mNegativeFilters.isEmpty()) {                    String negativePatterns = join(mNegativeFilters, ":");                    ((GeeTest)test).setPositiveFilters(negativePatterns);                }            }            // InstrumentationTest,大多数测试都是这个类型            // 应该很多人都不陌生            if (test instanceof InstrumentationTest) {                if (!mPositiveFilters.isEmpty()) {                    String annotation = join(mPositiveFilters, ",");                    ((InstrumentationTest)test).addInstrumentationArg(                            "annotation", annotation);                }                if (!mNegativeFilters.isEmpty()) {                    String notAnnotation = join(mNegativeFilters, ",");                    ((InstrumentationTest)test).addInstrumentationArg(                            "notAnnotation", notAnnotation);                }            }            forwardPackageDetails(testPackage.getPackageDef(), listener);            try {                // 重点在这里,执行测试setup,run,teardown                performPackagePrepareSetup(testPackage.getPackageDef());                test.run(filterMap.get(testPackage.getPackageDef().getId()));                performPackagePreparerTearDown(testPackage.getPackageDef());            } catch (DeviceUnresponsiveException due) {                // 出异常之后printStackTrace方便分析问题                ByteArrayOutputStream stack = new ByteArrayOutputStream();                due.printStackTrace(new PrintWriter(stack, true));                try {                    stack.close();                } catch (IOException ioe) {                }                ...            }            if (!mSkipConnectivityCheck) {                MonitoringUtils.checkDeviceConnectivity(getDevice(), listener,                        String.format("%s-%s", testPackage.getPackageDef().getName(),                                testPackage.getPackageDef().getAbi().getName()));            }            if (i < mTestPackageList.size() - 1) {                TestPackage nextPackage = mTestPackageList.get(i + 1);                rebootIfNecessary(testPackage, nextPackage);                changeToHomeScreen();            }            mLastTestPackageIndex = i;        }        if (mScreenshot) {            // 截图            InputStreamSource screenshotSource = getDevice().getScreenshot();            try {                listener.testLog("screenshot", LogDataType.PNG, screenshotSource);            } finally {                screenshotSource.cancel();            }        }        // 卸载之前预先安装上去的apk        uninstallPrequisiteApks(uninstallPackages);        // 收集log信息        collectReportLogs(getDevice(), mBuildInfo);    } catch (RuntimeException e) {        CLog.e(e);        throw e;    } catch (Error e) {        CLog.e(e);        throw e;    } finally {        for (ResultFilter filter : filterMap.values()) {            filter.reportUnexecutedTests();        }    }}

2.4.1 getAbis

去获取手机中的ro.product.cpu.abilist这个property:
找了个手机测试了下:[ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi]

Set<String> getAbis() throws DeviceNotAvailableException {    String bitness = (mForceAbi == null) ? "" : mForceAbi;    Set<String> abis = new HashSet<>();    for (String abi : AbiFormatter.getSupportedAbis(mDevice, bitness)) {        if (AbiUtils.isAbiSupportedByCompatibility(abi)) {            abis.add(abi);        }    }    return abis;}private static final String PRODUCT_CPU_ABILIST_KEY = "ro.product.cpu.abilist";private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";public static String[] getSupportedAbis(ITestDevice device, String bitness)        throws DeviceNotAvailableException {    // 获取property    String abiList = device.getProperty(PRODUCT_CPU_ABILIST_KEY + bitness);    if (abiList != null && !abiList.isEmpty()) {        String []abis = abiList.split(",");        if (abis.length > 0) {             return abis;         }     }     // fallback plan for before lmp, the bitness is ignored     return new String[]{device.getProperty(PRODUCT_CPU_ABI_KEY)}; }

2.4.2 setupTestPackageList

private void setupTestPackageList(Set<String> abis) throws DeviceNotAvailableException {    try {        // 这行代码中解析了plan文件        // 并拿到了配置文件中的所有的package        // 又对所有的package进行了xml解析封装成对象        ITestPackageRepo testRepo = createTestCaseRepo();        List<ITestPackageDef> testPkgDefs = new ArrayList<>(getAvailableTestPackages(testRepo));        testPkgDefs = filterByAbi(testPkgDefs, abis);        Collections.sort(testPkgDefs);        // 前面xml解析后封装成的TestPackageDef,转变为test        List<TestPackage> testPackageList = new ArrayList<>();        for (ITestPackageDef testPackageDef : testPkgDefs) {            IRemoteTest testForPackage = testPackageDef.createTest(mCtsBuild.getTestCasesDir());            if (testPackageDef.getTests().size() > 0) {                testPackageList.add(new TestPackage(testPackageDef, testForPackage));            }        }        ...    }}

2.4.3 createTestCaseRepo

可以说整个case组织的重点就在这一个方法中,重点分析

ITestPackageRepo createTestCaseRepo() {    return new TestPackageRepo(mCtsBuild.getTestCasesDir(), mIncludeKnownFailures);}public TestPackageRepo(File testCaseDir, boolean includeKnownFailures) {    mTestMap = new HashMap<>();    mIncludeKnownFailures = includeKnownFailures;    // 重点在这里,也就是说在TestCaseRepo创建的时候    // 就已经把testDir已经解析完毕了    // 已经把所有的xml中配置的测试case都装进了内存    parse(testCaseDir);}

parse中对测试目录的所有的.xml文件进行了遍历,并对每个xml文件进行解析以及封装:

private void parseModuleTestConfigs(File xmlFile)  {    TestPackageXmlParser parser = new TestPackageXmlParser(mIncludeKnownFailures);    try {        // 这个都不陌生了,sax解析        // 具体的解析逻辑在TestPackageXmlParser中        parser.parse(createStreamFromFile(xmlFile));        // 对这个xml同名称的.config文件解析        // 如果存在的话,会把其中配置的内容拿出来,在执行这条测试之前执行        // 大部分是执行这条测试需要预先执行的命令        File preparer = getPreparerDefForPackage(xmlFile);        IConfiguration config = null;        if (preparer != null) {            try {                config = ConfigurationFactory.getInstance().createConfigurationFromArgs(                        new String[]{preparer.getAbsolutePath()});            } catch (ConfigurationException e) {                throw new RuntimeException(                        String.format("error parsing config file: %s", xmlFile.getName()), e);            }        }        // 拿到在这个xml文件中解析出来的要执行的TestPackageDef        // 一个TestPackageDef就是上面的一个xml文件        Set<TestPackageDef> defs = parser.getTestPackageDefs();        if (defs.isEmpty()) {            Log.w(LOG_TAG, String.format("Could not find test package info in xml file %s",                    xmlFile.getAbsolutePath()));        }        for (TestPackageDef def : defs) {            String name = def.getAppPackageName();            String abi = def.getAbi().getName();            if (config != null) {                // 把config文件中需要执行的prepare操作记录下来                def.setPackagePreparers(config.getTargetPreparers());            }            // mTestMap是一个全局的大map            // 一级key是abi,二级key是测试的packageName,value是TestPackageDef对象            if (!mTestMap.containsKey(abi)) {                mTestMap.put(abi, new HashMap<String, TestPackageDef>());            }            // 放入全局map            mTestMap.get(abi).put(name, def);        }    } catch (FileNotFoundException e) {        Log.e(LOG_TAG, String.format("Could not find test case xml file %s",                xmlFile.getAbsolutePath()));        Log.e(LOG_TAG, e);    } catch (ParseException e) {        Log.e(LOG_TAG, String.format("Failed to parse test case xml file %s",                xmlFile.getAbsolutePath()));        Log.e(LOG_TAG, e);    }}

经过了这一步,我们可以总结出来:在createTestCaseRepo一步中:

  • 创建了TestCaseRepo
  • 对测试case所在的目录中所有的配置文件进行了xml解析
  • 每个xml文件中的Test标签都代表一条测试,每个xml文件对应一个TestPackageDef
  • 对需要prepare操作的package还进行了config的解析
  • 把所有的xml文件解析完毕之后,放到了一个二级全局map中
  • 不同的abi执行的测试完全是两个集合

2.4.4 getAvailableTestPackages

经过前面一步,已经拿到了所有的测试package的集合。
getAvailableTestPackages中的方法很长,这里只说明一下比较重要的部分:

Set<ITestPackageDef> testPkgDefs = new LinkedHashSet<>();if (mPlanName != null) {    // 拿到plan文件    File ctsPlanFile = mCtsBuild.getTestPlanFile(mPlanName);    ITestPlan plan = createPlan(mPlanName);    // 又是xml文件的解析,不过这次是plan文件    plan.parse(createXmlStream(ctsPlanFile));    // 这个testId是abi以及packagename拼接的字符串    // 分开之后就可以去上面的全局map获取testPackageDef    for (String testId : plan.getTestIds()) {        if (mExcludedPackageNames.contains(AbiUtils.parseTestName(testId))) {            continue;        }        ITestPackageDef testPackageDef = testRepo.getTestPackage(testId);        if (testPackageDef == null) {            continue;        }        // 去上面的全局测试package中获取        testPackageDef.setTestFilter(plan.getTestFilter(testId));        // 把这个plan中所有需要执行的测试testPackageDef取出来        testPkgDefs.add(testPackageDef);    }}

前面既然已经拿到了所有的可执行的测试case的全局map,这个地方就是根据测试的plan,根据plan中配置的packageName以及abi去全局map中获取这个plan中需要执行的测试case,然后组成一个list。

小结:上面分析了这么多,在CtsTest的run方法中其实就是一行代码setupTestPackageList(abiSet);经过了这行代码,已经测试目录下所有配置的xml文件解析完毕,并且根据本次测试的plan文件拿到了这个plan中要执行的测试case的list。

可能大脑堆栈有点深了,现在再回到run方法中继续:

2.4.5 runTest

run方法中虽然还有一些其他的方法,但基本都是针对测试之前的预处理了,包括getPrerequisiteApkscollectDeviceInfoprepareReportLogContainers等,这里就不一一列出其实现了,有兴趣可以自己去看,最重要的测试case的组织已经介绍过了。

performPackagePrepareSetup(testPackage.getPackageDef());test.run(filterMap.get(testPackage.getPackageDef().getId()));performPackagePreparerTearDown(testPackage.getPackageDef());

这里就是测试case了,也就是说CTS测试框架在基础框架的基础上进行了一系列的封装,在test组件中做的就是把测试case组织了以下以及plan的生成,最终还是又提供了测试模板方法。

2.5 测试类型

测试case有很多种类型,因此在上面的配置文件封装成对象之后还有最重要的一步就是:TestPackageDef.createTest
这里不列代码了,主要说明下测试类型:
测试一共有八种类型:

hostSideOnly:主要在主机端完成,测试代码通过jar包的方式提供,通过反射调用,测试内容主要是可以通过adb命令直接完成,比如install或push文件等。
native:测试包中推提供可运行文件,名称是测试的包名,测试时先将可执行文件push到手机上,然后赋予权限并执行。
wrappednative:目前只有一个opengl的测试是这个类型,是通过instrument来执行测试的,先安装apk,然后”am instrument -w xxx”命令来执行测试。
vmHostTest:这个类型目前也只有android.core.vm-tests-tf这一个测试,也是通过jar包的方式提供case,然后push到手机中通过junit测试。
deqpTest:通过am instrument来执行
uiAutomator:目前只有CtsUiAutomatorTests是这种方式,先将jar包推送到手机中然后通过am instrument的方式运行测试。
jUnitDeviceTest:目前只有CtsJdwp这个使用这个,也是通过jar包的方式提供,然后在手机中运行运行jar包。
CtsInstrumentationApkTest(默认测试类型):先安装apk,然后instrument来调用测试case。

3 执行测试

上面已经说明了详细的测试种类,大致执行方式上面也已经列出了。
对于hostside以及clientTest两种其实都需要手机与PC之前的通信,那么具体的通信细节是怎么实现的呢?
这就全靠我们都常用却忽视的一个jar包:ddmslib。可以去Google网站下下载源码,也可以直接反编译现有的jar包。
个人也没有研究太多,这里先列一些主要代码的实现逻辑,后面再详细研究:
原理就是直接通过socket跟adbd通信
AndroidDebugBridge

// Where to find the ADB bridge.static final String ADB_HOST = "127.0.0.1";static final int ADB_PORT = 5037;private static void initAdbSocketAddr() {    try {        int adb_port = determineAndValidateAdbPort();        sHostAddr = InetAddress.getByName(ADB_HOST);        sSocketAddr = new InetSocketAddress(sHostAddr, adb_port);    } catch (UnknownHostException e) {        // localhost should always be known.    }}

提供命令的执行的封装:AdbHelper

static void executeRemoteCommand(InetSocketAddress adbSockAddr,    String command, IDevice device, IShellOutputReceiver rcvr, long maxTimeToOutputResponse,    TimeUnit maxTimeUnits) throws TimeoutException, AdbCommandRejectedException,    ShellCommandUnresponsiveException, IOException {    ...    SocketChannel adbChan = null;    try {        adbChan = SocketChannel.open(adbSockAddr);        adbChan.configureBlocking(false);        // if the device is not -1, then we first tell adb we're looking to        // talk        // to a specific device        setDevice(adbChan, device);        byte[] request = formAdbRequest("shell:" + command);         write(adbChan, request);        // 写入要执行的命令请求        write(adbChan, request);        AdbResponse resp = readAdbResponse(adbChan, false /* readDiagString */);        if (!resp.okay) {            Log.e("ddms", "ADB rejected shell command (" + command + "): " + resp.message);            throw new AdbCommandRejectedException(resp.message);        }        byte[] data = new byte[16384];        ByteBuffer buf = ByteBuffer.wrap(data);        long timeToResponseCount = 0;        while (true) {            int count;            if (rcvr != null && rcvr.isCancelled()) {                Log.v("ddms", "execute: cancelled");                break;            }            // 读取数据            count = adbChan.read(buf);            if (count < 0) {                // we're at the end, we flush the output                rcvr.flush();                        + count);                break;            } else if (count == 0) {                try {                    int wait = WAIT_TIME * 5;                    timeToResponseCount += wait;                    if (maxTimeToOutputMs > 0 && timeToResponseCount > maxTimeToOutputMs) {                        throw new ShellCommandUnresponsiveException();                    }                    Thread.sleep(wait);                } catch (InterruptedException ie) {                }            } else {                timeToResponseCount = 0;                // send data to receiver if present                if (rcvr != null) {                    rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position());                }                buf.rewind();            }        }    } finally {        if (adbChan != null) {            adbChan.close();        }        Log.v("ddms", "execute: returning");    }}

只要是PC跟Android设备之间的通信,基本都是基于adb的,前面提到的各种测试的基础都是跟adbd之间的socket通信完成的。

总结

CTS测试框架在基础框架的基础上虽然修改的东西还是不少,但是可以看出来其实还是组件中内容的自定义,整体的基础框架的执行流程并没有变化。最重要的就是其中对于case的组织,提供各种xml文件以及plan去组织case。能把几十万条case都组织起来,说明这个框架也确实强大,但是缺点也很明显,随着测试Android的不断迭代,case越来越多,不仅仅是plan需要修改,xml文件也需要不断的增加,维护起来工作量会越来越大。因此就有了更加好用的V2版本。

下篇文章介绍V2版本。