CTS测试框架V2 - hursion/Android-CTS GitHub Wiki

CTS测试脚本解读

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 "$@"

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

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相关的一些配置。 通过这个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。

组件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);
    }
}

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文件怎么来的?

⚠️ **GitHub.com Fallback** ⚠️