CTS测试框架 -- V2版本

来源:互联网 发布:淘宝店招素材免费下载 编辑:程序博客网 时间:2024/06/15 18:08

目录

  • 概述
  • 组织case
    • 入口CompatibilityConsole
    • ModuleRepo
    • 组件CompatibilityTest
    • 执行测试
  • 总结

1 概述

在Android 6.0以及之前的版本上,CTS测试使用的都是前面介绍的V1框架,上篇文章已经介绍了V1框架的组织case的方式以及不足,主要是当测试case不断的增加之后带来的配置文件的不断变大,各模块之间的接耦合成都还是不够,因此就有了V2版本。
V2版本的组件名称都已经不以CTS作为名称了,变成了以Compatibility作为关键词,从名称上也可以大致看出来这个测试框架应该是变得更加通用了,CTS只是其中一个。其实,事实也确实是如此,因为随着Android O的发布,Google还推出了一个新的测试计划 – VTS,其实这个VTS框架的变化很小,基本就是在V2版本上稍加配置完成的,在本篇文章的结尾也会介绍下VTS测试框架的组成。

2 组织case

框架再变,主要功能还是不变,就是为了把那么多的case更好的组织起来,顺利跑完所有的测试case,因此重点还是在测试case的组织上。

2.1 入口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的提示,之前的版本是写死在代码中的,而这里就不一样了:

@Overrideprotected String getConsolePrompt() {    return String.format("%s-tf > ", SuiteInfo.NAME.toLowerCase());}

其中的SuiteInfo并不是某一个固定的java文件,而是动态编译生成的:

代码位置:/cts/build/compatibility_test_suite.mk

这个mk文件定义了SuiteInfo文件的生成:

# Generate the SuiteInfo.javasuite_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.javaLOCAL_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。
入口这个文件还是比较简单,就是作为整个框架的启动入口,自定义命令的添加。

2.2 ModuleRepo

在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相关的一些配置。
在V1版本中测试case的组织是通过TestPackageRepo,而这里是通过这个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。

2.3 组件CompatibilityTest

老套路,还是先看下这个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);    }}

可以发现这个run方法中并没有想V1版本的框架一样,根据plan文件去组织case,但是运行命令的时候plan参数还是可用的,这是怎么回事呢?
其实是V1和V2的很重要的一点区别就是在这里:
v1版本的plan文件相当于是一个集合,需要执行哪些测试case呢,就把需要执行的测试case添加到plan中,最后把这个集合中的测试case拿出来执行即可。
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。

2.4 执行测试

前面已经看到,在CompatibilityTest中执行测试的执行了module.run,这个方法就是去执行测试了,但是好像跟V1版本不一样啊,V1版本因为所有的case都已经在xml文件中注明了,解析完毕就已经知道要执行的测试case了,所以其实直接就进入了执行命令的步骤了,然而这里还是不一样,在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的配置文件,联想到框架,其实从本质上v1和v2的区别就在这里,v1是通过框架把所有的每一条测试case都拿到,逐个去执行,但是V2则是把每个测试module都作为一个configuration,框架做的就是去拿到测试的所有module,但是每个module的执行还是走了框架,因为每个module现在都被认为是一个configuration了,只需要去逐个执行测试module即可。

4 总结

到这里V2框架也说的差不多了,V2框架介绍没有贴太多的代码,一方面是本身V2框架相V1的组织逻辑就简单一些,另外一方面是因为两者的概念不同,v2把每个module作为一个configuration去处理,各个module之间都是独立的,并不需要像V1框架那样再需要一个xml文件去配置,需要plan文件去做一个集合。
另外VTS其实入口也是CompatibilityConsole,运行方式跟这个一样,包括前面SuiteInfo文件的生成,也是跟CTS如出一辙。

下篇文章介绍下添加case与自定义,以及系列总结。

原创粉丝点击