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_provider
,test
,logger
等组件的定义,最重要的还是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方法中虽然还有一些其他的方法,但基本都是针对测试之前的预处理了,包括getPrerequisiteApks
,collectDeviceInfo
,prepareReportLogContainers
等,这里就不一一列出其实现了,有兴趣可以自己去看,最重要的测试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版本。
- CTS测试框架 -- V1版本
- CTS测试框架 -- V2版本
- CTS测试框架 -- 开篇
- CTS测试框架 -- RegexTrie
- CTS测试框架 -- 总结
- CTS测试框架 -- 命令解析
- CTS测试框架 -- 命令调度
- CTS测试框架 -- 命令执行
- CTS测试框架 -- 基础框架Trade-Federation
- CTS测试框架 -- 基础框架启动
- Android兼容性测试框架(CTS)手册
- ANDROID兼容性测试框架(CTS)手册
- Android兼容性测试框架(CTS)手册
- Android兼容性测试框架(CTS)手册
- Android兼容性测试框架(CTS)手册
- Android兼容性测试框架(CTS)手册
- Android兼容性测试框架(CTS)手册
- cts 测试
- tensorflow.cast参数及功能
- Fuzzing简介以及使用AFL对LibTIFF进行模糊测试
- rails官方指南--建一个简易博客
- C++操作MySQL
- Java中怎么把科学计数法显示出全部数字
- CTS测试框架 -- V1版本
- 希尔排序
- java热部署与热加载,以及配置tomcat实现热部署的方式
- 渣渣四级
- 最新gitlab在CentOs7.3中的安装
- 调用winform添加音乐或图片
- leetcode 524. Longest Word in Dictionary through Deleting 子序列的确定
- python读取目录下所有文件
- pytorch使用:目录