VTS框架分析

CompatibilityConsole初始化

  VTS测试套件的执行脚本是通过直接加载com.android.compatibility.common.tradefed.command.CompatibilityConsole来进入交互命令行的:

android-vts/tools/vts-tradefed

cd ${VTS_ROOT}/android-vts/testcases/; java $RDBG_FLAG -cp ${JAR_PATH} -DVTS_ROOT=${VTS_ROOT} com.android.compatibility.common.tradefed.command.CompatibilityConsole "$@"

  CompatibilityConsole的main函数(以下源码基于Android8.1:

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/command/CompatibilityConsole.java

    public static void main(String[] args) throws InterruptedException, ConfigurationException {
        Console console = new CompatibilityConsole();
        Console.startConsole(console, args);
    }

  CompatibilityConsole类继承自Console类,在Console的构造函数里会初始化命令正则表达式匹配规则。其中addDefaultCommands提供了内置命令的匹配规则;setCustomCommands提供了自定义命令的匹配规则,目前实现为空;
generateHelpListings提供了帮助命令的匹配规则。

tools/tradefederation/core/src/com/android/tradefed/command/Console.java

    protected Console() {
        this(getReader());
    }

    /**
     * Create a {@link Console} with provided console reader.
     * Also, set up console command handling.
     * <p/>
     * Exposed for unit testing
     */
    Console(ConsoleReader reader) {
        super("TfConsole");
        mConsoleStartTime = System.currentTimeMillis();
        mConsoleReader = reader;
        if (reader != null) {
            mConsoleReader.addCompletor(
                    new ConfigCompletor(getConfigurationFactory().getConfigList()));
        }

        List<String> genericHelp = new LinkedList<String>();
        Map<String, String> commandHelp = new LinkedHashMap<String, String>();
        addDefaultCommands(mCommandTrie, genericHelp, commandHelp);
        setCustomCommands(mCommandTrie, genericHelp, commandHelp);
        generateHelpListings(mCommandTrie, genericHelp, commandHelp);
    }

  以常用的run命令(run vts …)举个例子。run命令对应一个ArgRunnable,run或run command后面的参数会通过CommandScheduler#addCommand添加到CommandScheduler等待处理。一个命令表达式对应一个Runnable或者ArgRunnable,像“run”,“run command”和“set log-level-display”这种命令后面还需要加上额外参数的就对应一个ArgRunnable;像“list commands”这种不需要额外参数的就对应一个Runnable。

tools/tradefederation/core/src/com/android/tradefed/command/Console.java

        // Run commands
        ArgRunnable<CaptureList> runRunCommand = new ArgRunnable<CaptureList>() {
            @Override
            public void run(CaptureList args) {
                // The second argument "command" may also be missing, if the
                // caller used the shortcut.
                int startIdx = 1;
                if (args.get(1).isEmpty()) {
                    // Empty array (that is, not even containing an empty string) means that
                    // we matched and skipped /(?:singleC|c)ommand/
                    startIdx = 2;
                }

                String[] flatArgs = new String[args.size() - startIdx];
                for (int i = startIdx; i < args.size(); i++) {
                    flatArgs[i - startIdx] = args.get(i).get(0);
                }
                try {
                    mScheduler.addCommand(flatArgs);
                } catch (ConfigurationException e) {
                    printLine("Failed to run command: " + e.toString());
                }
            }
        };
        trie.put(runRunCommand, RUN_PATTERN, "c(?:ommand)?", null);
        trie.put(runRunCommand, RUN_PATTERN, null);

  Console的构造函数初始化了命令的匹配规则,而startConsole则是启动Console这个线程(Console继承自Thread类)。startConsole第一个Console参数为上面提到过的CompatibilityConsole实例,第二个参数args为执行vts-tradefed脚本带的额外参数,在我们平时使用中一般为空,用来创建全局配置(GlobalConfiguration),非全局配置的部分就添加到CompatibilityConsole实例的启动参数中。之后再为CompatibilityConsole实例添加CommandScheduler和KeyStoreFactory,通过Console#run运行线程。

tools/tradefederation/core/src/com/android/tradefed/command/Console.java

    public static void startConsole(Console console, String[] args) throws InterruptedException,
            ConfigurationException {
        for (int i = 0;i < args.length;i++) {
            System.out.print(String.format("startConsole:%s",args[i]));
			System.out.println();
        }
        
        List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args);

		for(int i = 0;i <nonGlobalArgs.size();i++) {
			System.out.print(String.format("nonGlobalArgs:%s",nonGlobalArgs.get(i)));
			System.out.println();
	    }

        console.setArgs(nonGlobalArgs);
        console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler());
        console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory());
        console.setDaemon(true);
        console.start();

        // Wait for the CommandScheduler to get started before we exit the main thread.  See full
        // explanation near the top of #run()
        console.awaitScheduler();
    }

  Console#run首先启动通过CommandScheduler#start来启动CommandScheduler,因为CommandScheduler类也继承自Thread类。如果在运行vts-tradefed脚本带入了额外参数,就将这些额外参数当作输入参数进行处理;如果没有额外参数,就在命令交互行读取用户输入作为输入参数。输入参数经过之前提到的匹配规则处理后,会得到一个Runnable对象。以“run vts -m VtsVndkDependencyTest”命令为例,“run”会匹配一个ArgRunnable,剩下的“vts -m VtsVndkDependencyTest”会被放到一个CaptureList中,这个ArgRunnable和CaptureList会被作为参数传入到executeCmdRunnable里面去。又如“run command VtsVndkDependencyTest.config”命令,“run command”会匹配一个ArgRunnable,剩下的“VtsVndkDependencyTest.config”会被放到一个CaptureList中。

tools/tradefederation/core/src/com/android/tradefed/command/Console.java

    @Override
    public void run() {
        List<String> arrrgs = mMainArgs;

        if (mScheduler == null) {
            throw new IllegalStateException("command scheduler hasn't been set");
        }

        try {
            // Check System.console() since jline doesn't seem to consistently know whether or not
            // the console is functional.
            if (!isConsoleFunctional()) {
                if (arrrgs.isEmpty()) {
                    printLine("No commands for non-interactive mode; exiting.");
                    // FIXME: need to run the scheduler here so that the things blocking on it
                    // FIXME: will be released.
                    mScheduler.start();
                    mScheduler.await();
                    return;
                } else {
                    printLine("Non-interactive mode: Running initial command then exiting.");
                    mShouldExit = true;
                }
            }

            // Wait for the CommandScheduler to start.  It will hold the JVM open (since the Console
            // thread is a Daemon thread), and also we require it to have started so that we can
            // start processing user input.
            mScheduler.start();
            mScheduler.await();

            String input = "";
            CaptureList groups = new CaptureList();
            String[] tokens;

            // Note: since Console is a daemon thread, the JVM may exit without us actually leaving
            // this read loop.  This is by design.
            do {
                if (arrrgs.isEmpty()) {
                    input = getConsoleInput();

                    if (input == null) {
                        // Usually the result of getting EOF on the console
                        printLine("");
                        printLine("Received EOF; quitting...");
                        mShouldExit = true;
                        break;
                    }

                    tokens = null;
                    try {
						Log.d("TokenizeLine input",input);
                        tokens = QuotationAwareTokenizer.tokenizeLine(input);
					    for (int i = 0;i<tokens.length;i++) {
				            Log.d("TOKEN:",tokens[i]);
					    }
						
                    } catch (IllegalArgumentException e) {
                        printLine(String.format("Invalid input: %s.", input));
                        continue;
                    }

                    if (tokens == null || tokens.length == 0) {
                        continue;
                    }
                } else {
                    printLine(String.format("Using commandline arguments as starting command: %s",
                            arrrgs));
                    if (mConsoleReader != null) {
                        // Add the starting command as the first item in the console history
                        // FIXME: this will not properly escape commands that were properly escaped
                        // FIXME: on the commandline.  That said, it will still be more convenient
                        // FIXME: than copying by hand.
                        final String cmd = ArrayUtil.join(" ", arrrgs);
                        mConsoleReader.getHistory().addToHistory(cmd);
                    }
                    tokens = arrrgs.toArray(new String[0]);
                    if (arrrgs.get(0).matches(HELP_PATTERN)) {
                        // if started from command line for help, return to shell
                        mShouldExit = true;
                    }
                    arrrgs = Collections.emptyList();
                }

                Runnable command = mCommandTrie.retrieve(groups, tokens);
				for(int i=0;i<groups.size();i++) {
					List<String> stringList = groups.get(i);
					for (int j=0;j<stringList.size();j++) {
						printLine(String.format("CommandTrie retrieve:%s",stringList.get(j)));
					}
				}
                if (command != null) {
                    executeCmdRunnable(command, groups);
                } else {
                    printLine(String.format(
                            "Unable to handle command '%s'.  Enter 'help' for help.", tokens[0]));
                }
                RunUtil.getDefault().sleep(100);
            } while (!mShouldExit);
        } catch (Exception e) {
            printLine("Console received an unexpected exception (shown below); shutting down TF.");
            e.printStackTrace();
        } finally {
            mScheduler.shutdown();
            // Make sure that we don't quit with messages still in the buffers
            System.err.flush();
            System.out.flush();
        }
    }

  对于ArgRunnable,会以CaptureList为参数,调用其run函数;对于Runnable,直接调用其run函数。

tools/tradefederation/core/src/com/android/tradefed/command/Console.java

    @SuppressWarnings("unchecked")
    void executeCmdRunnable(Runnable command, CaptureList groups) {
        try {
            if (command instanceof ArgRunnable) {
                // FIXME: verify that command implements ArgRunnable<CaptureList> instead
                // FIXME: of just ArgRunnable
                ((ArgRunnable<CaptureList>) command).run(groups);
            } else {
                command.run();
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }

CommandScheduler处理命令

  前面看到run命令对应的ArgRunnable的run函数里面,会将run后面的参数通过CommandScheduler#addCommand添加到队列中。CommandScheduler#addCommand最终会调用到CommandScheduler#internalAddCommand。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

    private boolean internalAddCommand(String[] args, long totalExecTime, String cmdFilePath)
            throws ConfigurationException {
        assertStarted();
        CLog.d("internalAddCommand-->%s",ArrayUtil.join(" ", (Object[])args));
        IConfiguration config = createConfiguration(args);
        if (config.getCommandOptions().isHelpMode()) {
            getConfigFactory().printHelpForConfig(args, true, System.out);
        } else if (config.getCommandOptions().isFullHelpMode()) {
            getConfigFactory().printHelpForConfig(args, false, System.out);
        } else if (config.getCommandOptions().isJsonHelpMode()) {
            try {
                // Convert the JSON usage to a string (with 4 space indentation) and print to stdout
                System.out.println(config.getJsonCommandUsage().toString(4));
            } catch (JSONException e) {
                CLog.logAndDisplay(LogLevel.ERROR, "Failed to get json command usage: %s", e);
            }
        } else if (config.getCommandOptions().isDryRunMode()) {
            config.validateOptions();
            String cmdLine = QuotationAwareTokenizer.combineTokens(args);
            CLog.d("Dry run mode; skipping adding command: %s", cmdLine);
            if (config.getCommandOptions().isNoisyDryRunMode()) {
                System.out.println(cmdLine.replace("--noisy-dry-run", ""));
                System.out.println("");
            }
        } else {
            config.validateOptions();

            if (config.getCommandOptions().runOnAllDevices()) {
                addCommandForAllDevices(totalExecTime, args, cmdFilePath);
            } else {
                CommandTracker cmdTracker = createCommandTracker(args, cmdFilePath);
                cmdTracker.incrementExecTime(totalExecTime);
                ExecutableCommand cmdInstance = createExecutableCommand(cmdTracker, config, false);
                addExecCommandToQueue(cmdInstance, 0);
            }
            return true;
        }
        return false;
    }

  首先通过createConfiguration创建专属的配置(Configuration),这个后面再讲。根据配置和传入的命令参数创建一个ExecutableCommand,通过addExecCommandToQueue添加到mReadyCommands中,然后通过WaitObj#signalEventReceived唤醒阻塞的CommandScheduler(之前提到过CommandScheduler在没有新命令加入时,会每隔30s唤醒一次看看有没有需要命令需要处理)。
  CommandScheduler处理命令的循环体如下。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

            while (!isShutdown()) {
                // wait until processing is required again
                CLog.logAndDisplay(LogLevel.INFO, "Ready to wait 30s");
                mCommandProcessWait.waitAndReset(mPollTime);
                checkInvocations();
                processReadyCommands(manager);
                postProcessReadyCommands();
            }

  CommandScheduler#processReadyCommands会首先对mReadyCommands里面的命令进行排序处理,总执行时间短的放在前面。然后遍历mReadyCommands里面的命令,在allocateDevices分配到可用的设备时,直接将命令添加到mExecutingCommands队列中;在allocateDevices分配不到可用的设备时,将命令加入到mUnscheduledWarning,等待有设备可用再将命令添加到mExecutingCommands队列中。遍历完后,通过startInvocation对mExecutingCommands中的命令进行处理。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

    protected void processReadyCommands(IDeviceManager manager) {
        CLog.d("processReadyCommands...");
        Map<ExecutableCommand, IInvocationContext> scheduledCommandMap = new HashMap<>();
        // minimize length of synchronized block by just matching commands with device first,
        // then scheduling invocations/adding looping commands back to queue
        synchronized (this) {
            // sort ready commands by priority, so high priority commands are matched first
            Collections.sort(mReadyCommands, new ExecutableCommandComparator());
            Iterator<ExecutableCommand> cmdIter = mReadyCommands.iterator();
            while (cmdIter.hasNext()) {
                ExecutableCommand cmd = cmdIter.next();
                IConfiguration config = cmd.getConfiguration();
                IInvocationContext context = new InvocationContext();
                context.setConfigurationDescriptor(config.getConfigurationDescription());
                Map<String, ITestDevice> devices = allocateDevices(config, manager);
				Iterator<Map.Entry<String, ITestDevice>> it = devices.entrySet().iterator();
				while (it.hasNext()) {
					Map.Entry<String, ITestDevice> entry = it.next();
					System.out.println("key= " + entry.getKey());
                }
                if (!devices.isEmpty()) {
                    cmdIter.remove();
                    mExecutingCommands.add(cmd);
                    context.addAllocatedDevice(devices);

                    // track command matched with device
                    scheduledCommandMap.put(cmd, context);
                    // clean warned list to avoid piling over time.
                    mUnscheduledWarning.remove(cmd);
                } else {
                    if (!mUnscheduledWarning.contains(cmd)) {
                        CLog.logAndDisplay(LogLevel.DEBUG, "No available device matching all the "
                                + "config's requirements for cmd id %d.",
                                cmd.getCommandTracker().getId());
                        // make sure not to record since it may contains password
                        System.out.println(
                                String.format(
                                        "The command %s will be rescheduled.",
                                        Arrays.toString(cmd.getCommandTracker().getArgs())));
                        mUnscheduledWarning.add(cmd);
                    }
                }
            }
        }

        // now actually execute the commands
        for (Map.Entry<ExecutableCommand, IInvocationContext> cmdDeviceEntry : scheduledCommandMap
                .entrySet()) {
            ExecutableCommand cmd = cmdDeviceEntry.getKey();
            startInvocation(cmdDeviceEntry.getValue(), cmd,
                    new FreeDeviceHandler(getDeviceManager()));
            if (cmd.isLoopMode()) {
                addNewExecCommandToQueue(cmd.getCommandTracker());
            }
        }
        CLog.d("done processReadyCommands...");
    }

  CommandScheduler#startInvocation启动了一个线程InvocationThread来执行命令。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

    private void startInvocation(
            IInvocationContext context,
            ExecutableCommand cmd,
            IScheduledInvocationListener... listeners) {
        initInvocation();

        // Check if device is not used in another invocation.
        throwIfDeviceInInvocationThread(context.getDevices());

        CLog.d("starting invocation for command id %d", cmd.getCommandTracker().getId());
        // Name invocation with first device serial
        final String invocationName = String.format("Invocation-%s",
                context.getSerials().get(0));
		CLog.d("create an invocation thread:%s",invocationName);
        InvocationThread invocationThread = new InvocationThread(invocationName, context, cmd,
                listeners);
        logInvocationStartedEvent(cmd.getCommandTracker(), context);
        invocationThread.start();
        addInvocationThread(invocationThread);
    }

配置的创建

  刚才看到CommandScheduler#createConfiguration会根据传进来的参数进行配置的创建,例如执行“run vts -m VtsVndkDependencyTest”命令时,“vts -m VtsVndkDependencyTest”这几个参数就会被传进CommandScheduler#createConfiguration进行配置的创建。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

    private IConfiguration createConfiguration(String[] args) throws ConfigurationException {
        // check if the command should be sandboxed
        if (isCommandSandboxed(args)) {
            // Create an sandboxed configuration based on the sandbox of the scheduler.
            ISandbox sandbox = createSandbox();
            return SandboxConfigurationFactory.getInstance()
                    .createConfigurationFromArgs(args, getKeyStoreClient(), sandbox, new RunUtil());
        }
        return getConfigFactory().createConfigurationFromArgs(args, null, getKeyStoreClient());
    }

  createConfigurationFromArgs主要分为两步:1.通过internalCreateConfigurationFromArgs创建一个配置(configuration);2.通过setOptionsFromCommandLineArgs设置选项(option)的值。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

    @Override
    public IConfiguration createConfigurationFromArgs(String[] arrayArgs,
            List<String> unconsumedArgs, IKeyStoreClient keyStoreClient)
            throws ConfigurationException {
        List<String> listArgs = new ArrayList<String>(arrayArgs.length);
        // FIXME: Update parsing to not care about arg order.
        String[] reorderedArrayArgs = reorderArgs(arrayArgs);
        IConfiguration config =
                internalCreateConfigurationFromArgs(reorderedArrayArgs, listArgs, keyStoreClient);
        config.setCommandLine(arrayArgs);
        if (listArgs.contains("--" + CommandOptions.DRY_RUN_OPTION)) {
            // In case of dry-run, we replace the KeyStore by a dry-run one.
            CLog.w("dry-run detected, we are using a dryrun keystore");
            keyStoreClient = new DryRunKeyStore();
        }
        final List<String> tmpUnconsumedArgs = config.setOptionsFromCommandLineArgs(
                listArgs, keyStoreClient);

        if (unconsumedArgs == null && tmpUnconsumedArgs.size() > 0) {
            // (unconsumedArgs == null) is taken as a signal that the caller
            // expects all args to
            // be processed.
            throw new ConfigurationException(String.format(
                    "Invalid arguments provided. Unprocessed arguments: %s", tmpUnconsumedArgs));
        } else if (unconsumedArgs != null) {
            // Return the unprocessed args
            unconsumedArgs.addAll(tmpUnconsumedArgs);
        }

        return config;
    }

  先看看创建配置的过程。传入的参数的第一个会被作为配置文件的名字(此处“vts -m VtsVndkDependencyTest ”第一个为“vts”,所以第一个加载的配置文件为vts.xml),通过getConfigurationDef来生成一个ConfigurationDef。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

    private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
            List<String> optionArgsRef, IKeyStoreClient keyStoreClient)
            throws ConfigurationException {
        if (arrayArgs.length == 0) {
            throw new ConfigurationException("Configuration to run was not specified");
        }
        final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
        // first arg is config name
        final String configName = listArgs.remove(0);
		Log.d(LOG_TAG,"configName:"+configName);

        // Steal ConfigurationXmlParser arguments from the command line
        final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
        final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
        if (keyStoreClient != null) {
            templateArgParser.setKeyStore(keyStoreClient);
        }
        optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs));
        ConfigurationDef configDef = getConfigurationDef(configName, false,
                parserSettings.templateMap);
        if (!parserSettings.templateMap.isEmpty()) {
            // remove the bad ConfigDef from the cache.
            for (ConfigId cid : mConfigDefMap.keySet()) {
                if (mConfigDefMap.get(cid) == configDef) {
                    CLog.d("Cleaning the cache for this configdef");
                    mConfigDefMap.remove(cid);
                    break;
                }
            }
            throw new ConfigurationException(String.format("Unused template:map parameters: %s",
                    parserSettings.templateMap.toString()));
        }
        return configDef.createConfiguration();
    }

  可以看到,对于vts.xml,这里是使用了ConfigurationXmlParser#parse进行解析的。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

    ConfigurationDef getConfigurationDef(
            String name, boolean isGlobal, Map<String, String> templateMap)
            throws ConfigurationException {
        return new ConfigLoader(isGlobal).getConfigurationDef(name, templateMap);
    }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

        @Override
        public ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap)
                throws ConfigurationException {

            String configName = name;
            if (!isBundledConfig(name)) {
                configName = getAbsolutePath(null, name);
                // If the config file does not exist in the default location, try to locate it from
                // test cases directories defined by environment variables.
                File configFile = new File(configName);
                if (!configFile.exists()) {
                    configFile = getTestCaseConfigPath(name);
                    if (configFile != null) {
                        configName = configFile.getAbsolutePath();
                    }
                }
            }

            final ConfigId configId = new ConfigId(name, templateMap);
            ConfigurationDef def = mConfigDefMap.get(configId);

            if (def == null || def.isStale()) {
                def = new ConfigurationDef(configName);
                loadConfiguration(configName, def, null, templateMap);
                mConfigDefMap.put(configId, def);
            } else {
                if (templateMap != null) {
                    // Clearing the map before returning the cached config to
                    // avoid seeing them as unused.
                    templateMap.clear();
                }
            }
            return def;
        }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

        void loadConfiguration(
                String name,
                ConfigurationDef def,
                String deviceTagObject,
                Map<String, String> templateMap)
                throws ConfigurationException {
            System.out.format("Loading configuration %s\n",name);
            BufferedInputStream bufStream = getConfigStream(name);
            ConfigurationXmlParser parser = new ConfigurationXmlParser(this, deviceTagObject);
            parser.parse(def, name, bufStream, templateMap);

            // Track local config source files
            if (!isBundledConfig(name)) {
                def.registerSource(new File(name));
            }
        }

  解析xml过程如下。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationXmlParser.java

    void parse(ConfigurationDef configDef, String name, InputStream xmlInput,
            Map<String, String> templateMap) throws ConfigurationException {
        try {
            SAXParserFactory parserFactory = SAXParserFactory.newInstance();
            parserFactory.setNamespaceAware(true);
            SAXParser parser = parserFactory.newSAXParser();
            ConfigHandler configHandler =
                    new ConfigHandler(
                            configDef, name, mConfigDefLoader, mParentDeviceObject, templateMap);
            parser.parse(new InputSource(xmlInput), configHandler);
            checkValidMultiConfiguration(configHandler);
        } catch (ParserConfigurationException e) {
            throwConfigException(name, e);
        } catch (SAXException e) {
            throwConfigException(name, e);
        } catch (IOException e) {
            throwConfigException(name, e);
        }
    }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationXmlParser.java

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            if (OBJECT_TAG.equals(localName)) {
                final String objectTypeName = attributes.getValue("type");
                if (objectTypeName == null) {
                    throw new SAXException(new ConfigurationException(
                            "<object> must have a 'type' attribute"));
                }
                if (GlobalConfiguration.isBuiltInObjType(objectTypeName) ||
                        Configuration.isBuiltInObjType(objectTypeName)) {
                    throw new SAXException(new ConfigurationException(String.format("<object> "
                            + "cannot be type '%s' this is a reserved type.", objectTypeName)));
                }
                addObject(objectTypeName, attributes);
            } else if (DEVICE_TAG.equals(localName)) {
                if (mCurrentDeviceObject != null) {
                    throw new SAXException(new ConfigurationException(
                            "<device> tag cannot be included inside another device"));
                }
                // tag is a device tag (new format) for multi device definition.
                String deviceName = attributes.getValue("name");
                if (deviceName == null) {
                    throw new SAXException(
                            new ConfigurationException("device tag requires a name value"));
                }
                if (deviceName.equals(ConfigurationDef.DEFAULT_DEVICE_NAME)) {
                    throw new SAXException(new ConfigurationException(String.format("device name "
                            + "cannot be reserved name: '%s'",
                            ConfigurationDef.DEFAULT_DEVICE_NAME)));
                }
                if (deviceName.contains(String.valueOf(OptionSetter.NAMESPACE_SEPARATOR))) {
                    throw new SAXException(new ConfigurationException(String.format("device name "
                            + "cannot contain reserved character: '%s'",
                            OptionSetter.NAMESPACE_SEPARATOR)));
                }
                isMultiDeviceConfigMode = true;
                mConfigDef.setMultiDeviceMode(true);
                mCurrentDeviceObject = deviceName;
                addObject(localName, attributes);
            } else if (Configuration.isBuiltInObjType(localName)) {
                // tag is a built in local config object
                if (isLocalConfig == null) {
                    isLocalConfig = true;
                } else if (!isLocalConfig) {
                    throwException(String.format(
                            "Attempted to specify local object '%s' for global config!",
                            localName));
                }

                if (mCurrentDeviceObject == null &&
                        Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
                    // Keep track of all the BuildInObj outside of device tag for final check
                    // if it turns out we are in multi mode, we will throw an exception.
                    mOutsideTag.add(localName);
                }
                // if we are inside a device object, some tags are not allowed.
                if (mCurrentDeviceObject != null) {
                    if (!Configuration.doesBuiltInObjSupportMultiDevice(localName)) {
                        // Prevent some tags to be inside of a device in multi device mode.
                        throw new SAXException(new ConfigurationException(
                                String.format("Tag %s should not be included in a <device> tag.",
                                        localName)));
                    }
                }
                addObject(localName, attributes);
            } else if (GlobalConfiguration.isBuiltInObjType(localName)) {
                // tag is a built in global config object
                if (isLocalConfig == null) {
                    // FIXME: config type should be explicit rather than inferred
                    isLocalConfig = false;
                } else if (isLocalConfig) {
                    throwException(String.format(
                            "Attempted to specify global object '%s' for local config!",
                            localName));
                }
                addObject(localName, attributes);
            } else if (OPTION_TAG.equals(localName)) {
                String optionName = attributes.getValue("name");
                if (optionName == null) {
                    throwException("Missing 'name' attribute for option");
                }

                String optionKey = attributes.getValue("key");
                // Key is optional at this stage.  If it's actually required, another stage in the
                // configuration validation will throw an exception.

                String optionValue = attributes.getValue("value");
                if (optionValue == null) {
                    throwException("Missing 'value' attribute for option '" + optionName + "'");
                }
                if (mCurrentConfigObject != null) {
                    // option is declared within a config object - namespace it with object class
                    // name
                    optionName = String.format("%s%c%s", mCurrentConfigObject,
                            OptionSetter.NAMESPACE_SEPARATOR, optionName);
                }
                if (mCurrentDeviceObject != null) {
                    // preprend the device name in extra if inside a device config object.
                    optionName = String.format("{%s}%s", mCurrentDeviceObject, optionName);
                }
                mConfigDef.addOptionDef(optionName, optionKey, optionValue, mName);
            } else if (CONFIG_TAG.equals(localName)) {
                String description = attributes.getValue("description");
                if (description != null) {
                    // Ensure that we only set the description the first time and not when it is
                    // loading the <include> configuration.
                    if (mConfigDef.getDescription() == null ||
                            mConfigDef.getDescription().isEmpty()) {
                        mConfigDef.setDescription(description);
                    }
                }
            } else if (INCLUDE_TAG.equals(localName)) {
                String includeName = attributes.getValue("name");
                if (includeName == null) {
                    throwException("Missing 'name' attribute for include");
                }
                try {
                    mConfigDefLoader.loadIncludedConfiguration(
                            mConfigDef, mName, includeName, mCurrentDeviceObject, mTemplateMap);
                } catch (ConfigurationException e) {
                    if (e instanceof TemplateResolutionError) {
                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
                                mConfigDef.getName(), includeName));
                    }
                    throw new SAXException(e);
                }
            } else if (TEMPLATE_INCLUDE_TAG.equals(localName)) {
                final String templateName = attributes.getValue("name");
                if (templateName == null) {
                    throwException("Missing 'name' attribute for template-include");
                }
                if (mCurrentDeviceObject != null) {
                    // TODO: Add this use case.
                    throwException("<template> inside device object currently not supported.");
                }

                String includeName = mTemplateMap.get(templateName);
                if (includeName == null) {
                    includeName = attributes.getValue("default");
                }
                if (includeName == null) {
                    throwTemplateException(mConfigDef.getName(), templateName);
                }
                // Removing the used template from the map to avoid re-using it.
                mTemplateMap.remove(templateName);
                try {
                    mConfigDefLoader.loadIncludedConfiguration(
                            mConfigDef, mName, includeName, null, mTemplateMap);
                } catch (ConfigurationException e) {
                    if (e instanceof TemplateResolutionError) {
                        throwException(String.format(INNER_TEMPLATE_INCLUDE_ERROR,
                                mConfigDef.getName(), includeName));
                    }
                    throw new SAXException(e);
                }
            } else {
                throw new SAXException(String.format(
                        "Unrecognized tag '%s' in configuration", localName));
            }
        }

  文字描述比较麻烦,直接拿执行“run vts -m VtsVndkDependencyTest ”命令加载配置过程来说吧。由于run后面的第一个单词是“vts”,所以首先加载的是vts.xml。如果你执行的是“run cts-on-gsi”,那么首先加载的是cts-on-gsi.xml。
  最外层的标签configuration表明这是一个配置文件。include标签标示会额外引入同目录下的vts-base.xml。option标签用来配置一些实例的参数。例如< option name=“compatibility:include-filter” value=“VtsTrebleVintfTest” />这一项,表示将alias为““compatibility”的类实例的"include-filter"参数设置为"VtsTrebleVintfTest”。

test/vts/tools/vts-tradefed/res/config/vts.xml

<configuration description="VTS Main Test Plan">
  <include name="vts-base" />
  <option name="plan" value="vts" />
  <option name="test-tag" value="vts" />
  <option name="vts-plan-result:plan-name" value="vts" />

  <option name="compatibility:test-arg" value="com.android.tradefed.testtype.VtsMultiDeviceTest:precondition-vintf-override:true" />

  <!-- For Treble-specific validations -->
  <option name="compatibility:include-filter" value="VtsTreblePlatformVersionTest" />
  <option name="compatibility:include-filter" value="VtsTrebleVintfTest" />

  <!-- From vts-hal-hidl.xml -->
  <option name="compatibility:include-filter" value="VtsHalBluetoothV1_0Target" />
  <option name="compatibility:include-filter" value="VtsHalBootV1_0Target" />
  <option name="compatibility:include-filter" value="VtsHalDumpstateV1_0Target" />
  ...

  CompatibilityTest 类使用@OptionClass注解,alias的值对应配置xml里面option项name值冒号前面的部分。部分成员变量参数则使用option标签或者命令行输入设置。 @option注解中,name值表示长命令,用于option中设置参数或者在命令行中使用“–”进行设置;shortname值表示短命令,只能在命令行中使用“-”进行设置;descrition是对该参数项的描述;importance有三个值可选:NEVER(表示不会出现在帮助信息中),IF_UNSET(如果没有默认值的话才会出现在帮助信息中),ALWAYS(总是出现在帮助信息中)。“run vts -m VtsVndkDependencyTest ”这个短命令,我们可以用另外两种方法实现“-m VtsVndkDependencyTest”的效果:1.改用“run vts --module VtsVndkDependencyTest ”命令;2.在任一会加载到的配置xml加上“<option name="compatibility:module value="VtsVndkDependencyTest " />”。

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java

@OptionClass(alias = "compatibility")
public class CompatibilityTest implements IDeviceTest, IShardableTest, IBuildReceiver,
        IStrictShardableTest, ISystemStatusCheckerReceiver, ITestCollector,
        IInvocationContextReceiver {
        ...
        @Option(name = INCLUDE_FILTER_OPTION,
        description = "the include module filters to apply.",
        importance = Importance.ALWAYS)
        private Set<String> mIncludeFilters = new HashSet<>();
        ...
        @Option(name = MODULE_OPTION,
        shortName = 'm',
        description = "the test module to run.",
        importance = Importance.IF_UNSET)
        private String mModuleName = null;

  device_recovery标签指定了一个处理设备进入recovery操作的类;logger标签指定了一个写入log的类;result_reporter标签指定了处理测试结果的类;target_preparer标签指定了预先准备工作的类,该标签里面可以用option子标签为预先准备工作类指定参数值;test标签指定了主测试入口类;build_provider标签指定了设备版本构建信息的提供者类。

test/vts/tools/vts-tradefed/res/config/vts-base.xml

<configuration description="VTS Main Test Plan">
  <device_recovery class="com.android.tradefed.device.WaitDeviceRecovery" />

  <option name="compatibility:test-arg" value="com.android.tradefed.testtype.AndroidJUnitTest:rerun-from-file:true" />
  <option name="compatibility:test-arg" value="com.android.tradefed.testtype.AndroidJUnitTest:fallback-to-serial-rerun:false" />
  <logger class="com.android.tradefed.log.FileLogger">
    <option name="log-level-display" value="WARN" />
  </logger>
  <option name="compatibility:skip-all-system-status-check" value="true" />
  <option name="max-log-size" value="200" />
  <object type="vts-vendor-config" class="com.android.tradefed.util.VtsVendorConfigFileUtil" />
  <result_reporter class="com.android.compatibility.common.tradefed.result.ConsoleReporter" />
  <result_reporter class="com.android.compatibility.common.tradefed.result.VtsResultReporter" />

  <template-include name="reporters" default="basic-reporters" />
  <target_preparer class="com.android.tradefed.targetprep.VtsTestPlanResultReporter" />
  <test class="com.android.compatibility.common.tradefed.testtype.CompatibilityTest" />

  <build_provider class="com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider" />
  <target_preparer class="com.android.tradefed.targetprep.VtsDeviceInfoCollector" />
</configuration>

  由上面解析xml代码,我们可以看到:对于指定了类的标签(如target_preparer,logger等),代码调用的是ConfigurationXmlParser#addObject;对于指定参数值的option标签,代码调用的ConfigurationDef#addOptionDef。
  先看看ConfigurationXmlParser#addObject,其实是调用了ConfigurationDef#addConfigObjectDef,构建了一个ConfigObjectDef放到mObjectClassMap里面。而ConfigurationDef#addOptionDef则是构建了一个OptionDef放到mOptionList里面。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationXmlParser.java

        void addObject(String objectTypeName, Attributes attributes) throws SAXException {
            if (Configuration.DEVICE_NAME.equals(objectTypeName)) {
                // We still want to add a standalone device without any inner object.
                String deviceName = attributes.getValue("name");
                if (!mListDevice.contains(deviceName)) {
                    mListDevice.add(deviceName);
                    mConfigDef.addConfigObjectDef(objectTypeName,
                            DeviceConfigurationHolder.class.getCanonicalName());
                    mConfigDef.addExpectedDevice(deviceName);
                }
            } else {
                String className = attributes.getValue("class");
                if (className == null) {
                    throwException(String.format("Missing class attribute for object %s",
                            objectTypeName));
                }
                if (mCurrentDeviceObject != null) {
                    // Add the device name as a namespace to the type
                    objectTypeName = mCurrentDeviceObject + OptionSetter.NAMESPACE_SEPARATOR
                            + objectTypeName;
                }
                int classCount = mConfigDef.addConfigObjectDef(objectTypeName, className);
                mCurrentConfigObject = String.format("%s%c%d", className,
                        OptionSetter.NAMESPACE_SEPARATOR, classCount);
            }
        }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationDef.java

    int addConfigObjectDef(String typeName, String className) {
        List<ConfigObjectDef> classList = mObjectClassMap.get(typeName);
        if (classList == null) {
            classList = new ArrayList<ConfigObjectDef>();
            mObjectClassMap.put(typeName, classList);
        }

        // Increment and store count for this className
        Integer freq = mClassFrequency.get(className);
        freq = freq == null ? 1 : freq + 1;
        mClassFrequency.put(className, freq);
        classList.add(new ConfigObjectDef(className, freq));

        return freq;
    }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationDef.java

    void addOptionDef(String optionName, String optionKey, String optionValue,
            String optionSource) {
        mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource));
    }

  回头看看ConfigurationFactory#internalCreateConfigurationFromArgs函数,在调用getConfigurationDef时会通过读取配置xml生成一个ConfigurationDef,其中配置项指定的类和option指定的项分别保存到mObjectClassMap和mOptionList里面。最终通过ConfigurationDef#createConfiguration生成一个Configuration。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationFactory.java

    private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
            List<String> optionArgsRef, IKeyStoreClient keyStoreClient)
            throws ConfigurationException {
        if (arrayArgs.length == 0) {
            throw new ConfigurationException("Configuration to run was not specified");
        }
        final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
        // first arg is config name
        final String configName = listArgs.remove(0);
		Log.d(LOG_TAG,"configName:"+configName);

        // Steal ConfigurationXmlParser arguments from the command line
        final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
        final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
        if (keyStoreClient != null) {
            templateArgParser.setKeyStore(keyStoreClient);
        }
        optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs));
        ConfigurationDef configDef = getConfigurationDef(configName, false,
                parserSettings.templateMap);
        if (!parserSettings.templateMap.isEmpty()) {
            // remove the bad ConfigDef from the cache.
            for (ConfigId cid : mConfigDefMap.keySet()) {
                if (mConfigDefMap.get(cid) == configDef) {
                    CLog.d("Cleaning the cache for this configdef");
                    mConfigDefMap.remove(cid);
                    break;
                }
            }
            throw new ConfigurationException(String.format("Unused template:map parameters: %s",
                    parserSettings.templateMap.toString()));
        }
        return configDef.createConfiguration();
    }

  首先是遍历mObjectClassMap的每一项,通过createObject为每一个指定的类通过反射创建一个实例。这些类的实例会被放置到Configuration的实例的mConfigMap成员里面。mConfigMap是一个以xml标签名为键,以元素类型的Object的list为
值的map,例如有多个target_preparer标签,那么对应的list就会放置这些类的实例的集合。

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationDef.java

    IConfiguration createConfiguration() throws ConfigurationException {
        IConfiguration config = new Configuration(getName(), getDescription());
        List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
        IDeviceConfiguration defaultDeviceConfig =
                new DeviceConfigurationHolder(DEFAULT_DEVICE_NAME);
        if (!mMultiDeviceMode) {
            // We still populate a default device config to avoid special logic in the rest of the
            // harness.
            deviceObjectList.add(defaultDeviceConfig);
        } else {
            for (String name : mExpectedDevices) {
                deviceObjectList.add(new DeviceConfigurationHolder(name));
            }
        }

        for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) {
            List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size());
            String entryName = objClassEntry.getKey();
            boolean shouldAddToFlatConfig = true;

            for (ConfigObjectDef configDef : objClassEntry.getValue()) {
                Object configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
                Matcher matcher = null;
                if (mMultiDeviceMode) {
                    matcher = MULTI_PATTERN.matcher(entryName);
                }
                if (mMultiDeviceMode && matcher.find()) {
                    // If we find the device namespace, fetch the matching device or create it if
                    // it doesn't exists.
                    IDeviceConfiguration multiDev = null;
                    shouldAddToFlatConfig = false;
                    for (IDeviceConfiguration iDevConfig : deviceObjectList) {
                        if (matcher.group(1).equals(iDevConfig.getDeviceName())) {
                            multiDev = iDevConfig;
                            break;
                        }
                    }
                    if (multiDev == null) {
                        multiDev = new DeviceConfigurationHolder(matcher.group(1));
                        deviceObjectList.add(multiDev);
                    }
                    // We reference the original object to the device and not to the flat list.
                    multiDev.addSpecificConfig(configObject);
                    multiDev.addFrequency(configObject, configDef.mAppearanceNum);
                } else {
                    if (Configuration.doesBuiltInObjSupportMultiDevice(entryName)) {
                        defaultDeviceConfig.addSpecificConfig(configObject);
                        defaultDeviceConfig.addFrequency(configObject, configDef.mAppearanceNum);
                    } else {
                        // Only add to flat list if they are not part of multi device config.
                        objectList.add(configObject);
                    }
                }
            }
            if (shouldAddToFlatConfig) {
                config.setConfigurationObjectList(entryName, objectList);
            }
        }
        // We always add the device configuration list so we can rely on it everywhere
        config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
        config.injectOptionValues(mOptionList);

        return config;
    }

tools/tradefederation/core/src/com/android/tradefed/config/ConfigurationDef.java

    private Object createObject(String objectTypeName, String className)
            throws ConfigurationException {
        try {
            Class<?> objectClass = getClassForObject(objectTypeName, className);
            Object configObject = objectClass.newInstance();
            return configObject;
        } catch (InstantiationException e) {
            throw new ConfigurationException(String.format(
                    "Could not instantiate class %s for config object type %s", className,
                    objectTypeName), e);
        } catch (IllegalAccessException e) {
            throw new ConfigurationException(String.format(
                    "Could not access class %s for config object type %s", className,
                    objectTypeName), e);
        }
    }

  接下来是injectOptionValues为上面mConfigMap的类实例设置对应的参数。getAllConfigurationObject会从mConfigMap将所有类实例的集合返回,然后以这个集合为参数构造一个OptionSetter。通过internalInjectOptionValue,会将
mOptionList中保存的option参数设置到对应类实例的参数中去。

tools/tradefederation/core/src/com/android/tradefed/config/Configuration.java

    @Override
    public void injectOptionValues(List<OptionDef> optionDefs) throws ConfigurationException {
        OptionSetter optionSetter = createOptionSetter();
        for (OptionDef optionDef : optionDefs) {
            internalInjectOptionValue(optionSetter, optionDef.name, optionDef.key, optionDef.value,
                    optionDef.source);
        }
    }

tools/tradefederation/core/src/com/android/tradefed/config/Configuration.java

    private OptionSetter createOptionSetter() throws ConfigurationException {
        return new OptionSetter(getAllConfigurationObjects());
    }

tools/tradefederation/core/src/com/android/tradefed/config/Configuration.java

    private Collection<Object> getAllConfigurationObjects(String excludedConfigName) {
        Collection<Object> objectsCopy = new ArrayList<Object>();
        for (Entry<String, List<Object>> entryList : mConfigMap.entrySet()) {
            if (excludedConfigName != null) {
                // Only add if not a descriptor config object type.
                if (!excludedConfigName.equals(entryList.getKey())) {
                    objectsCopy.addAll(entryList.getValue());
                }
            } else {
                objectsCopy.addAll(entryList.getValue());
            }
        }
        return objectsCopy;
    }

InvocationThread启动

  之前提到,执行命令会运行一个InvocationThread线程。可见该线程就是执行了TestInvocation#invoke函数。

tools/tradefederation/core/src/com/android/tradefed/command/CommandScheduler.java

        @Override
        public void run() {
            Map<ITestDevice, FreeDeviceState> deviceStates = new HashMap<>();
            for (ITestDevice device : mInvocationContext.getDevices()) {
                deviceStates.put(device, FreeDeviceState.AVAILABLE);
            }
            mStartTime = System.currentTimeMillis();
            ITestInvocation instance = getInvocation();
            IConfiguration config = mCmd.getConfiguration();

            // Copy the command options invocation attributes to the invocation.
            // TODO: Implement a locking/read-only mechanism to prevent unwanted attributes to be
            // added during the invocation.
            if (!config.getCommandOptions().getInvocationData().isEmpty()) {
                mInvocationContext.addInvocationAttributes(
                        config.getCommandOptions().getInvocationData());
            }

            try {
                mCmd.commandStarted();
                long invocTimeout = config.getCommandOptions().getInvocationTimeout();
                if (invocTimeout > 0) {
                    CLog.i("Setting a timer for the invocation in %sms", invocTimeout);
                    mExecutionTimer.schedule(mInvocationThreadMonitor, invocTimeout);
                }
                instance.invoke(mInvocationContext, config,
                        new Rescheduler(mCmd.getCommandTracker()), mListeners);
            } 

tools/tradefederation/core/src/com/android/tradefed/invoker/TestInvocation.java

    /** {@inheritDoc} */
    @Override
    public void invoke(
            IInvocationContext context,
            IConfiguration config,
            IRescheduler rescheduler,
            ITestInvocationListener... extraListeners)
            throws DeviceNotAvailableException, Throwable {
            ...
            String cmdLineArgs = config.getCommandLine();
            if (cmdLineArgs != null) {
                CLog.i("Invocation was started with cmd: %s", cmdLineArgs);
            }

            boolean providerSuccess = fetchBuild(context, config, rescheduler, listener);
            if (!providerSuccess) {
                return;
            }

            boolean sharding = shardConfig(config, context, rescheduler);
            if (sharding) {
                CLog.i("Invocation for %s has been sharded, rescheduling", context.getSerials());
                return;
            }

            if (config.getTests() == null || config.getTests().isEmpty()) {
                CLog.e("No tests to run");
                return;
            }

            performInvocation(config, context, rescheduler, listener);
            setExitCode(ExitCode.NO_ERROR, null);
            ...
    }

   最终会调用到TestInvocation#prepareAndRun。可以看到执行分成两步:1.setup阶段;2.runtests阶段。

tools/tradefederation/core/src/com/android/tradefed/invoker/TestInvocation.java

    private void prepareAndRun(
            IConfiguration config, IInvocationContext context, ITestInvocationListener listener)
            throws Throwable {
        if (config.getCommandOptions().shouldUseSandboxing()) {
            // TODO: extract in new TestInvocation type.
            // If the invocation is sandboxed run as a sandbox instead.
            SandboxInvocationRunner.prepareAndRun(config, context, listener);
            return;
        }
        getRunUtil().allowInterrupt(true);
        logDeviceBatteryLevel(context, "initial -> setup");
        doSetup(context, config, listener);
        logDeviceBatteryLevel(context, "setup -> test");
        runTests(context, config, listener);
        logDeviceBatteryLevel(context, "after test");
    }

setup阶段

  setup阶段就是将收集到的target_preparer和multi_target_preparer的实例,分别运行它们的setUp函数。

tools/tradefederation/core/src/com/android/tradefed/invoker/TestInvocation.java

    @Override
    public void doSetup(
            IInvocationContext context,
            IConfiguration config,
            final ITestInvocationListener listener)
            throws TargetSetupError, BuildError, DeviceNotAvailableException {
        // TODO: evaluate doing device setup in parallel
        for (String deviceName : context.getDeviceConfigNames()) {
            ITestDevice device = context.getDevice(deviceName);
            CLog.d("Starting setup for device: '%s'", device.getSerialNumber());
            if (device instanceof ITestLoggerReceiver) {
                ((ITestLoggerReceiver) context.getDevice(deviceName))
                        .setTestLogger(listener);
            }
            if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) {
                device.preInvocationSetup(context.getBuildInfo(deviceName));
            }
            for (ITargetPreparer preparer : config.getDeviceConfigByName(deviceName)
                    .getTargetPreparers()) {
                if (preparer instanceof ITestLoggerReceiver) {
                    ((ITestLoggerReceiver) preparer).setTestLogger(listener);
                }
                CLog.d(
                        "starting preparer '%s' on device: '%s'",
                        preparer, device.getSerialNumber());
                preparer.setUp(device, context.getBuildInfo(deviceName));
                CLog.d(
                        "done with preparer '%s' on device: '%s'",
                        preparer, device.getSerialNumber());
            }
            CLog.d("Done with setup of device: '%s'", device.getSerialNumber());
        }
        // After all the individual setup, make the multi-devices setup
        for (IMultiTargetPreparer multipreparer : config.getMultiTargetPreparers()) {
            if (multipreparer instanceof ITestLoggerReceiver) {
                ((ITestLoggerReceiver) multipreparer).setTestLogger(listener);
            }
            CLog.d("Starting multi target preparer '%s'", multipreparer);
            multipreparer.setUp(context);
            CLog.d("done with multi target preparer '%s'", multipreparer);
        }
        if (config.getProfiler() != null) {
            config.getProfiler().setUp(context);
        }
        // Upload setup logcat after setup is complete
        for (String deviceName : context.getDeviceConfigNames()) {
            reportLogs(context.getDevice(deviceName), listener, Stage.SETUP);
        }
    }

  举个例子,vts-base.xml里面指定了一个target_preparer类: com.android.tradefed.targetprep.VtsDeviceInfoCollector。它的setup函数会记录一些设备的ro属性例如ro.product.device,ro.product.cpu.abilist等等到设备的buildinfo里面。

test/vts/harnesses/tradefed/src/com/android/tradefed/targetprep/VtsDeviceInfoCollector.java

    @Override
    public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
            BuildError, DeviceNotAvailableException {
        for (Entry<String, String> entry : BUILD_KEYS.entrySet()) {
            buildInfo.addBuildAttribute(entry.getKey(),
                    ArrayUtil.join(",", device.getProperty(entry.getValue())));
        }
    }

runtests阶段

  runtests阶段就是收集配置中的test标签记录的类实例,执行它们的run函数:

tools/tradefederation/core/src/com/android/tradefed/invoker/TestInvocation.java

    @VisibleForTesting
    void runTests(
            IInvocationContext context, IConfiguration config, ITestInvocationListener listener)
            throws DeviceNotAvailableException {
        // Wrap collectors in each other and collection will be sequential
        for (IMetricCollector collector : config.getMetricCollectors()) {
            listener = collector.init(context, listener);
        }

        for (IRemoteTest test : config.getTests()) {
            // For compatibility of those receivers, they are assumed to be single device alloc.
            if (test instanceof IDeviceTest) {
                ((IDeviceTest)test).setDevice(context.getDevices().get(0));
            }
            if (test instanceof IBuildReceiver) {
                ((IBuildReceiver)test).setBuild(context.getBuildInfo(
                        context.getDevices().get(0)));
            }
            if (test instanceof ISystemStatusCheckerReceiver) {
                ((ISystemStatusCheckerReceiver) test).setSystemStatusChecker(
                        config.getSystemStatusCheckers());
            }

            // TODO: consider adding receivers for only the list of ITestDevice and IBuildInfo.
            if (test instanceof IMultiDeviceTest) {
                ((IMultiDeviceTest)test).setDeviceInfos(context.getDeviceBuildMap());
            }
            if (test instanceof IInvocationContextReceiver) {
                ((IInvocationContextReceiver)test).setInvocationContext(context);
            }
            test.run(listener);
        }
    }

  鉴于在本例中的test标签对应的类是com.android.compatibility.common.tradefed.testtype.CompatibilityTest,看看它的run函数,简单地分成两步:1.initializeModuleRepo初始化模块;2.分别调用符合条件的模块(ModuleDef)的run函数。

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java

    @Override
    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
            ...
            LinkedList<IModuleDef> modules = initializeModuleRepo();
            ...
            while (!modules.isEmpty()) {
                // Make sure we remove the modules from the reference list when we are done with
                // them.
                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 {
                        CLog.logAndDisplay(LogLevel.INFO, "Rebooting device before starting next "
                            + "module");
                        mDevice.reboot();
                    }
                }
                try {
                    module.run(listener);
                } catch (DeviceUnresponsiveException due) {
                ...
    }

模块初始化

  initializeModuleRepo会根据事先在命令行或者xml option选项的设置对CompatibilityTests参数的设定进行初始化。初始化过程为:首先遍历VTS套件的testcase目录(vts/android-vts/testcases)下的所有.config文件,对于可用的abi集合(一般为arm64-v8a和armeabi-v7a),都会根据.config文件前缀名和abi名通过AbiUtils#createId构建一个id,通过shouldRunModule函数来判断当前的id对应的模块(ModuleDef)是否需要跑。如果不需要,则continue直接跳过。
  如果当前id对应的模块需要进行测试,则会先通过ConfigurationFactory#createConfigurationFromArgs创建一个配置(configuration)。以我们运行的命令“run vts -m VtsVndkDependencyTest”为例,对应的config文件位于android-vts/testcases/VtsVndkDependencyTest.config。
  最后通过addModuleDef添加一个模块(ModuleDef)。

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java

    protected LinkedList<IModuleDef> initializeModuleRepo()
            throws DeviceNotAvailableException, FileNotFoundException {
        // FIXME: Each shard will do a full initialization which is not optimal. Need a way
        // to be more specific on what to initialize.
        synchronized (mModuleRepo) {
            if (!mModuleRepo.isInitialized()) {
                setupFilters();
                // Initialize the repository, {@link CompatibilityBuildHelper#getTestsDir} can
                // throw a {@link FileNotFoundException}
                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.");
            }
            // Get the tests to run in this shard
            return mModuleRepo.getModules(getDevice().getSerialNumber(), mShardIndex);
        }
    }

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java

    @Override
    public void initialize(int totalShards, Integer shardIndex, File testsDir, Set<IAbi> abis,
            List<String> deviceTokens, List<String> testArgs, List<String> moduleArgs,
            Set<String> includeFilters, Set<String> excludeFilters,
            MultiMap<String, String> metadataIncludeFilters,
            MultiMap<String, String> metadataExcludeFilters,
            IBuildInfo buildInfo) {
        CLog.d("Initializing ModuleRepo\nShards:%d\nTests Dir:%s\nABIs:%s\nDevice Tokens:%s\n" +
                "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
                totalShards, testsDir.getAbsolutePath(), abis, deviceTokens, testArgs, moduleArgs,
                includeFilters, excludeFilters);
        mInitialized = true;
        mTotalShards = totalShards;
        mShardIndex = shardIndex;
        synchronized (lock) {
            if (mTokenModuleScheduled == null) {
                mTokenModuleScheduled = new HashSet<>();
            }
        }

        for (String line : deviceTokens) {
            String[] parts = line.split(":");
            if (parts.length == 2) {
                String key = parts[0];
                String value = parts[1];
                Set<String> list = mDeviceTokens.get(key);
                if (list == null) {
                    list = new HashSet<>();
                    mDeviceTokens.put(key, list);
                }
                list.add(value);
            } else {
                throw new IllegalArgumentException(
                        String.format("Could not parse device token: %s", line));
            }
        }
        putArgs(testArgs, mTestArgs);
        putArgs(moduleArgs, mModuleArgs);
        mIncludeAll = includeFilters.isEmpty();
        // Include all the inclusions
        addFilters(includeFilters, mIncludeFilters, abis);
        // Exclude all the exclusions
        addFilters(excludeFilters, mExcludeFilters, abis);

        File[] configFiles = testsDir.listFiles(new ConfigFilter());
        if (configFiles.length == 0) {
            throw new IllegalArgumentException(
                    String.format("No config files found in %s", testsDir.getAbsolutePath()));
        }
        Map<String, Integer> shardedTestCounts = new HashMap<>();
        for (File configFile : configFiles) {
            final String name = configFile.getName().replace(CONFIG_EXT, "");
            final String[] pathArg = new String[] { configFile.getAbsolutePath() };
            try {
                // Invokes parser to process the test module config file
                // Need to generate a different config for each ABI as we cannot guarantee the
                // configs are idempotent. This however means we parse the same file multiple times
                for (IAbi abi : abis) {
                    String id = AbiUtils.createId(abi.getName(), name);
                    if (!shouldRunModule(id)) {
                        // If the module should not run tests based on the state of filters,
                        // skip this name/abi combination.
                        continue;
                    }

                    IConfiguration config = mConfigFactory.createConfigurationFromArgs(pathArg);
                    if (!filterByConfigMetadata(config,
                            metadataIncludeFilters, metadataExcludeFilters)) {
                        // if the module config did not pass the metadata filters, it's excluded
                        // from execution
                        continue;
                    }
                    Map<String, List<String>> args = new HashMap<>();
                    if (mModuleArgs.containsKey(name)) {
                        args.putAll(mModuleArgs.get(name));
                    }
                    if (mModuleArgs.containsKey(id)) {
                        args.putAll(mModuleArgs.get(id));
                    }
                    injectOptionsToConfig(args, config);

                    List<IRemoteTest> tests = config.getTests();
                    for (IRemoteTest test : tests) {
                        prepareTestClass(name, abi, config, test);
                    }
                    List<IRemoteTest> shardedTests = tests;
                    if (mTotalShards > 1) {
                         shardedTests = splitShardableTests(tests, buildInfo);
                    }
                    if (shardedTests.size() > 1) {
                        shardedTestCounts.put(id, shardedTests.size());
                    }
                    for (IRemoteTest test : shardedTests) {
                        addModuleDef(name, abi, test, pathArg);
                    }
                }
            } catch (ConfigurationException e) {
                throw new RuntimeException(String.format("error parsing config file: %s",
                        configFile.getName()), e);
            }
        }
        mExcludeFilters.clear();
        TestRunHandler.setTestRuns(new CompatibilityBuildHelper(buildInfo), shardedTestCounts);
    }

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java

    protected 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()));
    }

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java

    protected void addModuleDef(IModuleDef moduleDef) {
        Set<String> tokens = moduleDef.getTokens();
        if (tokens != null && !tokens.isEmpty()) {
            mTokenModules.add(moduleDef);
        } else {
            mNonTokenModules.add(moduleDef);
        }
    }

模块运行

  ModuleDef#run主要分成两步:1.runPreparerSetups用来执行收集到的target_preparer的setup函数;2.执行test标签的测试类的实例的run函数。

cts/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleDef.java

    @Override
    public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
        CLog.d("Running module %s", toString());
        runPreparerSetups();

        CLog.d("Test: %s", mTest.getClass().getSimpleName());
        prepareTestClass();

        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
        runPreparerTeardowns();
    }

  以“run vts -m VtsVndkDependencyTest”命令对应的为例。target_preparer分别有VtsFilePusher(用来push文件到设备里面)和VtsPythonVirtualenvPreparer(初始化python环境)。test主测试类为VtsMultiDeviceTest,并指定了测试python模块文件在"vts/testcases/vndk/dependency/VtsVndkDependencyTest"。

android-vts/testcases/VtsVndkDependencyTest.config

<configuration description="Config for VTS VNDK dependency test cases">
    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.VtsFilePusher">
        <option name="push-group" value="HostDrivenTest.push" />
    </target_preparer>
    <target_preparer class="com.android.tradefed.targetprep.VtsPythonVirtualenvPreparer">
    </target_preparer>
    <test class="com.android.tradefed.testtype.VtsMultiDeviceTest">
        <option name="test-module-name" value="VtsVndkDependencyTest" />
        <option name="test-case-path" value="vts/testcases/vndk/dependency/VtsVndkDependencyTest" />
    </test>
</configuration>

  VtsMultiDeviceTest使用“python -m python文件路径”的方法来执行python测试模块。

test/vts/harnesses/tradefed/src/com/android/tradefed/testtype/VtsMultiDeviceTest.java

    @SuppressWarnings("deprecation")
    @Override
    public void run(ITestInvocationListener listener)
            throws IllegalArgumentException, DeviceNotAvailableException {
        if (mDevice == null) {
            throw new DeviceNotAvailableException("Device has not been set");
        }

        if (mTestCasePath == null) {
            if (!mBinaryTestSource.isEmpty()) {
                String template;
                switch (mBinaryTestType) {
                    case BINARY_TEST_TYPE_GTEST:
                        template = TEMPLATE_GTEST_BINARY_TEST_PATH;
                        break;
                    case BINARY_TEST_TYPE_HAL_HIDL_GTEST:
                        template = TEMPLATE_HAL_HIDL_GTEST_PATH;
                        break;
                    case BINARY_TEST_TYPE_HOST_BINARY_TEST:
                        template = TEMPLATE_HOST_BINARY_TEST_PATH;
                        break;
                    default:
                        template = TEMPLATE_BINARY_TEST_PATH;
                }
                CLog.i("Using default test case template at %s.", template);
                setTestCasePath(template);
                if (mEnableCoverage && !mGlobalCoverage) {
                    CLog.e("Only global coverage is supported for test type %s.", mBinaryTestType);
                    throw new RuntimeException("Failed to produce VTS runner test config");
                }
            } else if (mBinaryTestType.equals(BINARY_TEST_TYPE_HAL_HIDL_REPLAY_TEST)) {
                setTestCasePath(TEMPLATE_HAL_HIDL_REPLAY_TEST_PATH);
            } else if (mBinaryTestType.equals(BINARY_TEST_TYPE_LLVMFUZZER)) {
                // Fuzz test don't need test-case-path.
                setTestCasePath(TEMPLATE_LLVMFUZZER_TEST_PATH);
            } else {
                throw new IllegalArgumentException("test-case-path is not set.");
            }
        }

        setPythonPath();

        if (mPythonBin == null) {
            mPythonBin = getPythonBinary();
        }

        if (mRunUtil == null){
            mRunUtil = new RunUtil();
            mRunUtil.setEnvVariable(PYTHONPATH, mPythonPath);
        }

        doRunTest(listener);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Invoker123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值