CTS测试框架V2 - hursion/Android-CTS GitHub Wiki
cts-tradefed框架时基于基础框架tradefederation(tools/tradefederation/)的二次封装,对于tradefederation感兴趣的可以去查阅https://source.android.com/devices/tech/test_infra/tradefed
android-cts-8.1_r8-linux_x86-x86为例,我们运行cts测试时,首先启动脚本cts-tradefed,这个脚本在源码中的位置:cts/tools/cts-tradefed/etc,编译后会copy到out/host/linux-x86/bin;
脚本中最核心的一句话就是启动TF窗口
java $RDBG_FLAG -Xmx4g -XX:+HeapDumpOnOutOfMemoryError -cp ${JAR_PATH} -DCTS_ROOT=${CTS_ROOT} com.android.compatibility.common.tradefed.command.CompatibilityConsole "$@"
代码位置:/cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/
CompatibilityConsole是基础框架中Console的子类,主要是添加了自定义Command,setCustomCommands这个方法中添加了V2框架支持自己支持的命令,可以看到在自定义的命令中多了一些关于module的命令,这个module非常重要,是整个V2框架的重心,在V2框架的测试case执行中,淡化了plan的概念,强调了这个module的概念,一个module就代表一组测试case,简单的理解,对于以apk为单位测试的case,一个apk就是一个module。 举个例子:其中的listModules
private void listModules() {
File[] files = null;
try {
// 获取测试目录
// 这个ModuleRepo.ConfigFilter主要作用就是获取所有config结尾的文件
files = getBuildHelper().getTestsDir().listFiles(new ModuleRepo.ConfigFilter());
} catch (FileNotFoundException e) {
printLine(e.getMessage());
e.printStackTrace();
}
if (files != null && files.length > 0) {
List<String> modules = new ArrayList<>();
for (File moduleFile : files) {
// 遍历目录下的所有文件
// 把config结尾的文件的文件名列出来
modules.add(FileUtil.getBaseName(moduleFile.getName()));
}
Collections.sort(modules);
for (String module : modules) {
printLine(module);
}
} else {
printLine("No modules found");
}
}
这个地方ModuleRepo是一个重点,测试case的组织全靠这个repo。
如果运行过CTS测试case的话,会知道在测试运行的时候控制台上命令提示符前面会有cts-tf的提示,之前的版本是写死在代码中的,而这里就不一样了:
@Override
protected String getConsolePrompt() {
return String.format("%s-tf > ", SuiteInfo.NAME.toLowerCase());
}
其中的SuiteInfo并不是某一个固定的java文件,而是动态编译生成的:
代码位置:/cts/build/compatibility_test_suite.mk
这个mk文件定义了SuiteInfo文件的生成:
# Generate the SuiteInfo.java
suite_info_java := $(call intermediates-dir-for,JAVA_LIBRARIES,$(LOCAL_MODULE),true,COMMON)/com/android/compatibility/SuiteInfo.java
$(suite_info_java): PRIVATE_SUITE_BUILD_NUMBER := $(LOCAL_SUITE_BUILD_NUMBER)
$(suite_info_java): PRIVATE_SUITE_TARGET_ARCH := $(LOCAL_SUITE_TARGET_ARCH)
$(suite_info_java): PRIVATE_SUITE_NAME := $(LOCAL_SUITE_NAME)
$(suite_info_java): PRIVATE_SUITE_FULLNAME := $(LOCAL_SUITE_FULLNAME)
$(suite_info_java): PRIVATE_SUITE_VERSION := $(LOCAL_SUITE_VERSION)
$(suite_info_java): cts/build/compatibility_test_suite.mk $(LOCAL_MODULE_MAKEFILE)
@echo Generating: $@
$(hide) mkdir -p $(dir $@)
$(hide) echo "/* This file is auto generated by Android.mk. Do not modify. */" > $@
$(hide) echo "package com.android.compatibility;" >> $@
$(hide) echo "public class SuiteInfo {" >> $@
$(hide) echo " public static final String BUILD_NUMBER = \"$(PRIVATE_SUITE_BUILD_NUMBER)\";" >> $@
$(hide) echo " public static final String TARGET_ARCH = \"$(PRIVATE_SUITE_TARGET_ARCH)\";" >> $@
$(hide) echo " public static final String NAME = \"$(PRIVATE_SUITE_NAME)\";" >> $@
$(hide) echo " public static final String FULLNAME = \"$(PRIVATE_SUITE_FULLNAME)\";" >> $@
$(hide) echo " public static final String VERSION = \"$(PRIVATE_SUITE_VERSION)\";" >> $@
$(hide) echo "}" >> $@
# Include the SuiteInfo.java
LOCAL_GENERATED_SOURCES := $(suite_info_java)
这个文件重点就是生成了一些常量,而这些常量也正是在mk文件中定义的:比如我们上面说的SuiteInfo.NAME,生成过程:
public static final String NAME = \"$(PRIVATE_SUITE_NAME)\";" >> $@
$(suite_info_java): PRIVATE_SUITE_NAME := $(LOCAL_SUITE_NAME)
LOCAL_SUITE_NAME的定义:
/cts/tools/cts-tradefed/Android.mk
其中有一行LOCAL_SUITE_NAME := CTS 上面的文件中还有一些其他常量的定义。 虽然这个只是一个名称的定义,但是意义在于整个V2版本的框架的灵活程度变的更高了,尽可能的把更多的内容放在mk文件中定义,避免hard code。 入口这个文件还是比较简单,就是作为整个框架的启动入口,自定义命令的添加。
在V2版本的框架中淡化的plan的概念,取而代之是module,开始之前我们先看下这个module的config文件究竟长什么样,跟V1版本的有什么区别,依旧是CtsJobSchedulerTestCases:
<configuration description="Config for CTS Job Scheduler test cases">
<option name="config-descriptor:metadata" key="component" value="framework" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="CtsJobSchedulerTestCases.apk" />
<option name="test-file-name" value="CtsJobSchedulerJobPerm.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.jobscheduler.cts" />
<option name="runtime-hint" value="2m" />
</test>
</configuration>
可以看到,没有了之前每条测试都配置一个xml中的一个标签,取而代之的是这个module相关的一些配置。 通过这个ModuleRepo,在初始化的时候扫描测试目录下所有的config文件
private void addModuleDef(String name, IAbi abi, IRemoteTest test,
String[] configPaths) throws ConfigurationException {
// Invokes parser to process the test module config file
IConfiguration config = mConfigFactory.createConfigurationFromArgs(configPaths);
addModuleDef(new ModuleDef(name, abi, test, config.getTargetPreparers(),
config.getConfigurationDescription()));
}
每个config文件代表了一个module,把所有的config文件解析之后放入list。
老套路,还是先看下这个V2框架的组件配置文件:
代码位置 platform/cts/common/host-side/tradefed/res/config/ platform/cts/tools/cts-tradefed/res/config
配置文件比较多,这个地方就不贴代码了,但是核心没有变,组件的配置在/cts/common/host-side/tradefed/res/config/common-compatibility-config.xml中,有一个test组件的配置
<test class="com.android.compatibility.common.tradefed.testtype.CompatibilityTest" />
,可见V2框架的test组件就是这个CompatibilityTest,直奔其run方法:
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
try {
List<ISystemStatusChecker> checkers = new ArrayList<>();
// 系统状态检查
if (mSkipAllSystemStatusCheck) {
CLog.d("Skipping system status checkers");
} else {
checkSystemStatusBlackAndWhiteList();
for (ISystemStatusChecker checker : mListCheckers) {
if(shouldIncludeSystemStatusChecker(checker)) {
checkers.add(checker);
}
}
}
LinkedList<IModuleDef> modules;
synchronized (mModuleRepo) {
if (!mModuleRepo.isInitialized()) {
// 这步很重要,初始化了filter
// 一个代表要删除掉的module,一个代表要添加的额外module
setupFilters();
// ModuleRepo的初始化,已经添加了所有的测试case
mModuleRepo.initialize(mTotalShards, mShardIndex, mBuildHelper.getTestsDir(),
getAbis(), mDeviceTokens, mTestArgs, mModuleArgs, mIncludeFilters,
mExcludeFilters,
mModuleMetadataIncludeFilter, mModuleMetadataExcludeFilter,
mBuildHelper.getBuildInfo());
// Add the entire list of modules to the CompatibilityBuildHelper for reporting
mBuildHelper.setModuleIds(mModuleRepo.getModuleIds());
int count = UniqueModuleCountUtil.countUniqueModules(
mModuleRepo.getTokenModules()) +
UniqueModuleCountUtil.countUniqueModules(
mModuleRepo.getNonTokenModules());
CLog.logAndDisplay(LogLevel.INFO, "========================================");
CLog.logAndDisplay(LogLevel.INFO, "Starting a run with %s unique modules.",
count);
CLog.logAndDisplay(LogLevel.INFO, "========================================");
} else {
CLog.d("ModuleRepo already initialized.");
}
// 获取本次测试要跑的module的集合
modules = mModuleRepo.getModules(getDevice().getSerialNumber(), mShardIndex);
}
// clearFilter,就是前面提到的一个代表要删除掉的module,一个代表要添加的额外module
mExcludeFilters.clear();
mIncludeFilters.clear();
if (mRetrySessionId != null) {
loadRetryCommandLineArgs(mRetrySessionId);
}
listener = new FailureListener(listener, getDevice(), mBugReportOnFailure,
mLogcatOnFailure, mScreenshotOnFailure, mRebootOnFailure, mMaxLogcatBytes);
int moduleCount = modules.size();
if (moduleCount == 0) {
if (sPreparedLatch != null) {
sPreparedLatch.countDown();
}
return;
} else {
int uniqueModuleCount = UniqueModuleCountUtil.countUniqueModules(modules);
}
if (mRebootBeforeTest) {
mDevice.reboot();
}
if (mSkipConnectivityCheck) {
String clazz = NetworkConnectivityChecker.class.getCanonicalName();
mSystemStatusCheckBlacklist.add(clazz);
}
boolean isPrepared = true;
for (int i = 0; i < moduleCount; i++) {
IModuleDef module = modules.get(i);
module.setBuild(mBuildHelper.getBuildInfo());
module.setDevice(mDevice);
module.setPreparerWhitelist(mPreparerWhitelist);
// 开始对每个module设置组件以及device
if (mCollectTestsOnly != null) {
module.setCollectTestsOnly(mCollectTestsOnly);
}
isPrepared &= (module.prepare(mSkipPreconditions, mPreconditionArgs));
}
if (!isPrepared) {
throw new RuntimeException(String.format("Failed preconditions on %s",
mDevice.getSerialNumber()));
}
if (mIsLocalSharding) {
try {
sPreparedLatch.countDown();
int attempt = 1;
while(!sPreparedLatch.await(MINUTES_PER_PREP_ATTEMPT, TimeUnit.MINUTES)) {
if (attempt > NUM_PREP_ATTEMPTS ||
InvocationFailureHandler.hasFailed(mBuildHelper)) {
CLog.logAndDisplay(LogLevel.ERROR,
"Incorrect preparation detected, exiting test run from %s",
mDevice.getSerialNumber());
return;
}
CLog.logAndDisplay(LogLevel.WARN, "waiting on preconditions");
attempt++;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
mModuleRepo.tearDown();
mModuleRepo = null;
// 开始执行测试
while (!modules.isEmpty()) {
IModuleDef module = modules.poll();
long start = System.currentTimeMillis();
if (mRebootPerModule) {
if ("user".equals(mDevice.getProperty("ro.build.type"))) {
CLog.e("reboot-per-module should only be used during development, "
+ "this is a\" user\" build device");
} else {
mDevice.reboot();
}
}
// 运行测试检查
if (checkers != null && !checkers.isEmpty()) {
runPreModuleCheck(module.getName(), checkers, mDevice, listener);
}
IInvocationContext moduleContext = new InvocationContext();
moduleContext.setConfigurationDescriptor(module.getConfigurationDescriptor());
moduleContext.addInvocationAttribute(IModuleDef.MODULE_NAME, module.getName());
moduleContext.addInvocationAttribute(IModuleDef.MODULE_ABI,
module.getAbi().getName());
mInvocationContext.setModuleInvocationContext(moduleContext);
try {
// 执行module
module.run(listener);
} catch (DeviceUnresponsiveException due) {
// being able to catch a DeviceUnresponsiveException here implies that recovery
// was successful, and test execution should proceed to next module
ByteArrayOutputStream stack = new ByteArrayOutputStream();
due.printStackTrace(new PrintWriter(stack, true));
StreamUtil.close(stack);
} finally {
mInvocationContext.setModuleInvocationContext(null);
}
long duration = System.currentTimeMillis() - start;
long expected = module.getRuntimeHint();
long delta = Math.abs(duration - expected);
// Show warning if delta is more than 10% of expected
if (expected > 0 && ((float)delta / (float)expected) > 0.1f) {
CLog.logAndDisplay(LogLevel.WARN,
"Inaccurate runtime hint for %s, expected %s was %s",
module.getId(),
TimeUtil.formatElapsedTime(expected),
TimeUtil.formatElapsedTime(duration));
}
if (checkers != null && !checkers.isEmpty()) {
runPostModuleCheck(module.getName(), checkers, mDevice, listener);
}
module = null;
}
} catch (FileNotFoundException fnfe) {
throw new RuntimeException("Failed to initialize modules", fnfe);
}
}
v2版本默认就把所有的测试case给全部拿到并执行,除非配置了不需要执行哪些case,否则的话默认执行全部的case。 这就是执行cts这个plan的时候还是会跑全部的case的原因了,因为其实不管你在执行的时候plan是谁,都是跑全部的case,如果不想跑全部的case的话,就需要去特殊定制配置文件:比如cts-java.xml 其中配置了
<option name="compatibility:include-filter" value="CtsLibcoreTestCases" />
也就是说通过include-filter以及exclude-filter两个filter去特殊定制指定的plan。
前面已经看到,在CompatibilityTest中执行测试的执行了module.run,这个方法就是去执行测试了,在ModuleDef的run方法:
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
CLog.d("Running module %s", toString());
// Run DynamicConfigPusher setup once more, in case cleaner has previously
// removed dynamic config file from the target (see b/32877809)
for (ITargetPreparer preparer : mDynamicConfigPreparers) {
runPreparerSetup(preparer);
}
// Setup
for (ITargetPreparer preparer : mPreparers) {
runPreparerSetup(preparer);
}
CLog.d("Test: %s", mTest.getClass().getSimpleName());
if (mTest instanceof IAbiReceiver) {
((IAbiReceiver) mTest).setAbi(mAbi);
}
if (mTest instanceof IBuildReceiver) {
((IBuildReceiver) mTest).setBuild(mBuild);
}
if (mTest instanceof IDeviceTest) {
((IDeviceTest) mTest).setDevice(mDevice);
}
IModuleListener moduleListener = new ModuleListener(this, listener);
// Guarantee events testRunStarted and testRunEnded in case underlying test runner does not
ModuleFinisher moduleFinisher = new ModuleFinisher(moduleListener);
mTest.run(moduleFinisher);
moduleFinisher.finish();
// Tear down
for (ITargetCleaner cleaner : mCleaners) {
CLog.d("Cleaner: %s", cleaner.getClass().getSimpleName());
cleaner.tearDown(mDevice, mBuild, null);
}
}
看这个地方好像有些似曾相识,再看前面的config的配置文件,联想到框架,V2是把每个测试module都作为一个configuration,框架做的就是去拿到测试的所有module,但是每个module的执行还是走了框架,因为每个module现在都被认为是一个configuration了,只需要去逐个执行测试module即可。
v2把每个module作为一个configuration去处理,各个module之间都是独立的。 另外VTS其实入口也是CompatibilityConsole,运行方式跟这个一样,包括前面SuiteInfo文件的生成,也是跟CTS如出一辙。
config文件怎么来的?