(24)-case的组织

最后更新于:2022-04-01 06:55:14

# UML图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911de0380c7.jpg) # 解释 上图涉及5个类:`ddmlib`提供的`TestIdentifier`类,`cts`自定义的`IRemoteTest`接口、`ITestPackageDef`接口、`TestFilter`类和`TestPackage`类。具体意义如下: ## TestIdentifier `ddmlib`为我们提供的属于`instrumentation`测试的`case`实体类,里面有2个属性,case的类名和方法名。这样我们就可以找到一个case。 ## IRemoteTest 一种测试类型的接口定义,该测试类型可以直接将结果发送到监听器,让其处理。所以测试runner部分只管撒了欢的跑,抓结果的模块是单独存在的。 ## ITestPackageDef case相关信息的容器,信息从哪里来?举个例子,写`uiautomator case`的人都了解,我们写的case都要打成jar包,随着jar一起生成的还有一个与jar包名一样的xml文件,我们的信息就是从该xml文件里来,里面定义了jar包的标识符,测试包名等信息。 ## TestFilter case的过滤器类,定义一些过滤的条件。里面有2个集合保存了要被过滤掉的case的类名或`TestIdentifier`对象,以及1个特殊的类名和方法名,这个属性默认是为`null`,一般情况下需要你去设值,如果不设置,那么这个条件就不作为过滤的条件,来具体看看删选的处理代码: ~~~ public Collection<TestIdentifier> filter(Collection<TestIdentifier > tests) { List<TestIdentifier> filteredTests = new ArrayList<TestIdentifier>(tests.size()); for (TestIdentifier test : tests) { if (mIncludedClass != null && !test.getClassName().equals(mIncludedClass)) { // skip continue; } if (mIncludedMethod != null && !test.getTestName().equals(mIncludedMethod)) { // skip continue; } if (mExcludedClasses.contains(test.getClassName())) { // skip continue; } if (mExcludedTests.contains(test)) { // skip continue; } filteredTests.add(test); } Collections.sort(filteredTests, new TestIdComparator()); return filteredTests; } ~~~ 代码块很简单,随着条件分支一步一步过滤,最后剩下来的case添加到新集合中返回。 ## TestPackage 包含上面3个实体对象(除了`TestFilter`),一个TestPackage代表了一个case包(jar或者apk等)相关的所有信息,例如一个`uiautomator`写出的jar包,那么一个jar包就需要定义一个`TestPackage`对象:该jar包包含的case集合,该jar包执行的测试类型,以及jar一些相关属性信息。有了这些就足够了,case就可以执行run的动作了。cts执行的时候只需要得到TestPackage对象集合(代表一个个的case包对象),就可以遍历得到所有要执行的case。 # 具体执行过程 cts中是以plan来定义要跑的case包集合,plan则是一个xml文件,里面定义了一个或多个case包的标识信息。这样去case的目录下就可以找到case包以及case包的xml配置文件。当我们传入一个plan进入cts后,发生了什么?(一下方法都是CtsTest中的方法) ## buildTestsToRun方法 当测试执行的时候,cts会先调用该方法获得所有TestPackage对象,上面说过,获得这个就足够了。具体该实现: ~~~ private List<TestPackage> buildTestsToRun() { List<TestPackage> testPkgList = new LinkedList<TestPackage>(); try { // 获得testcases目录下所有的xml文件解析出来的case包对象 ITestPackageRepo testRepo = createTestCaseRepo(); // 得到本次plan所需跑的case Collection<ITestPackageDef> testPkgDefs = getTestPackagesToRun(testRepo); for (ITestPackageDef testPkgDef : testPkgDefs) { addTestPackage(testPkgList, testPkgDef); } if (testPkgList.isEmpty()) { Log.logAndDisplay(LogLevel.WARN, LOG_TAG, "No tests to run"); } } catch (FileNotFoundException e) { throw new IllegalArgumentException("failed to find CTS plan file", e); } catch (ParseException e) { throw new IllegalArgumentException("failed to parse CTS plan file", e); } catch (ConfigurationException e) { throw new IllegalArgumentException("failed to process arguments", e); } return testPkgList; } ~~~ 首先会去将固定路径下的(cts根目录下的repository\testcases)下所有xml文件解析出来,这些xml文件都是和case包一一对应的,你不能去解析jar包或者apk包吧,所以需要一个case包配置文件的存在,这样我们读取xml的信息就可以得到相关的信息。然后我们要筛选出本次plan需要跑的case包。 ## getTestPackagesToRun方法 该方法里有4条分支,每条分支代表不同的执行任务的标识。 `1`以plan名定义的任务 `2`.以case包的uri定义的集合所定义的任务 `3`.以class定义的(一个类中的所有case)任务 `4`.以sessionID(cts为之前跑过的任务都定义了一个session)所定义的任务,这个是重跑之前的任务。 我们来只看第一种,以plan方式启动的任务。 ~~~ private Collection<ITestPackageDef> getTestPackagesToRun(ITestPackageRepo testRepo) throws ParseException, FileNotFoundException, ConfigurationException { // use LinkedHashSet to have predictable iteration order Set<ITestPackageDef> testPkgDefs = new LinkedHashSet<ITestPackageDef>(); if (mPlanName != null) { Log.i(LOG_TAG, String.format("Executing CTS test plan %s", mPlanName)); File ctsPlanFile = mCtsBuildHelper.getTestPlanFile(mPlanName); ITestPlan plan = createPlan(mPlanName); plan.parse(createXmlStream(ctsPlanFile)); for (String uri : plan.getTestUris()) { if (!mExcludedPackageNames.contains(uri)) { ITestPackageDef testPackage = testRepo.getTestPackage(uri); testPackage.setExcludedTestFilter(plan.getExcludedTestFilter(uri)); testPkgDefs.add(testPackage); } } } else if (mPackageNames.size() > 0) { ...... } else if (mClassName != null) { ...... } else if (mContinueSessionId != null) { ...... } else { // should never get here - was checkFields() not called? throw new IllegalStateException("nothing to run?"); } return testPkgDefs; } ~~~ 该分支中,首先根据plan名找到所有xml文件,然后解析xml文件,得到case。这样我们就得到了ITestPackageDef对象,遍历得到所有这样的对象,然后将该对象集合返回。回到上面的`buildTestsToRun()`中,然后根据返回的集合元素创建`TestPackage`对象集合,这样我们的处理过程就完成了。
';

(23)-设备分类

最后更新于:2022-04-01 06:55:12

[上一篇文章](http://testerhome.com/topics/1858)已经讲了cts如何自动检测到设备,效果就是无需我们再去调用`ADB`的`getIDevice()`得到设备,利用的是ADB中提供的观察者模式做到了这一点,那么得到设备后我们如何对这些设备进行管理的呢? # 设备分类 cts中将设备分为3种状态:处于验证中的设备,可用设备,执行任务的设备。这三种状态的设备分别用3个集合保存: ~~~ //处于验证中的设备集合 private Map<String, IDeviceStateMonitor> mCheckDeviceMap; //可用设备的集合 private ConditionPriorityBlockingQueue<IDevice> mAvailableDeviceQueue; //执行任务的设备集合 private Map<String, IManagedTestDevice> mAllocatedDeviceMap; ~~~ ## 1.处于验证中的设备集合 `mCheckDeviceMap`是一个`Map`,`key`值表示设备的`SN`号,`value`值表示当前设备的状态监听器对象`IDeviceStateMonitor`(以后会讲到)。当一个新设备被检测的时候会首先放到该Map中,然后会调用`IDeviceStateMonitor`.`waitForDeviceShell(final long waitTime)`来对设备进行一个扫描,扫描通过以后,就会将设备添加到可用设备集合`mAvailableDeviceQueue`中。但是不管扫描成不成功,经过检测步骤后,都会从`mCheckDeviceMap`设备集合中删除该设备。所以可以说该容器这是临时存放设备用的,为的是对设备进行验证是否可用,就像机场里过安检一样。 ## 2.可用的设备集合 `mAvailableDeviceQueue`是一个[优先队列](http://baike.baidu.com/view/1267829.htm),且是线程安全的,这是`cts`自定义的一个队列数据结构。你可以传入条件选择设备,比如按设备号选择,按平台号选择都可以。返回的是一个`IDevice`对象,这是原生的`ADB`中定义的接口类。该集合是一个中间集合,它从`mCheckDeviceMap`中得到集合,然后等到用户使用设备后,就将集合中的某个元素"送给"了执行任务的设备集合`mAllocatedDeviceMap`。 ## 3.执行任务的设备集合 `mAllocatedDeviceMap`集合是一个Map,`key`值表示设备的`SN`号,`value`值表示的是cts自己定义的设备对象的接口`IManagedTestDevice`。当用户选择一个可用设备后,是从可用设备集合`mAvailableDeviceQueue`中得到了一个`IDevice`,然后`cts`使用外观模式,将其封装到了继承自`IManagedTestDevice`接口的对象`TestDevice`(这个类很重要,以后会单独讲)中,里面有很多关于设备的方法可以被调用,那么用户实际能操作就是这个`TestDevice`类。等待任务完成后,该设备将“送还”到可用设备集合中,这只是一个借用的过程,用完了就还给了它,这样的话这个设备还可以继续执行其他任务。 ## 4.总结 通过上面的介绍,我们用一幅图来描述一下3个集合之间的关系。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddf21518.jpg) # 检测设备后分类 终于到了揭晓庐山真面目的时候,以上的种种解释,包括第一篇文章的铺垫,都是为了下面的内容铺垫的,讲代码的东西就是这么的麻烦。先上代码: ~~~ private class ManagedDeviceListener implements IDeviceChangeListener { /** * {@inheritDoc} */ @Override public void deviceChanged(IDevice device, int changeMask) { IManagedTestDevice testDevice = mAllocatedDeviceMap.get(device.getSerialNumber()); if ((changeMask & IDevice.CHANGE_STATE) != 0) { if (testDevice != null) { TestDeviceState newState = TestDeviceState.getStateByDdms(device.getState()); testDevice.setDeviceState(newState); } else if (mCheckDeviceMap.containsKey(device.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(device.getSerialNumber()); monitor.setState(TestDeviceState.getStateByDdms(device.getState())); } else if (!mAvailableDeviceQueue.contains(device) && device.getState() ==IDevice.DeviceState.ONLINE) { checkAndAddAvailableDevice(device); } } } /** * {@inheritDoc} */ @Override public void deviceConnected(IDevice device) { CLog.d("Detected device connect %s, id %d", device.getSerialNumber(), device.hashCode()); IManagedTestDevice testDevice = mAllocatedDeviceMap.get(device.getSerialNumber()); if (testDevice == null) { if (isValidDeviceSerial(device.getSerialNumber()) && device.getState() == IDevice.DeviceState.ONLINE) { checkAndAddAvailableDevice(device); } else if (mCheckDeviceMap.containsKey(device.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(device.getSerialNumber()); monitor.setState(TestDeviceState.getStateByDdms(device.getState())); } } else { // this device is known already. However DDMS will allocate a // new IDevice, so need // to update the TestDevice record with the new device CLog.d("Updating IDevice for device %s", device.getSerialNumber()); testDevice.setIDevice(device); TestDeviceState newState = TestDeviceState.getStateByDdms(device.getState()); testDevice.setDeviceState(newState); } } private boolean isValidDeviceSerial(String serial) { return serial.length() > 1 && !serial.contains("?"); } /** * {@inheritDoc} */ @Override public void deviceDisconnected(IDevice disconnectedDevice) { if (mAvailableDeviceQueue.remove(disconnectedDevice)) { CLog.i("Removed disconnected device %s from available queue",disconnectedDevice.getSerialNumber()); } IManagedTestDevice testDevice = mAllocatedDeviceMap.get(disconnectedDevice.getSerialNumber()); if (testDevice != null) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } else if (mCheckDeviceMap.containsKey(disconnectedDevice.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(disconnectedDevice.getSerialNumber()); monitor.setState(TestDeviceState.NOT_AVAILABLE); } updateDeviceMonitor(); } } ~~~ 上面的代码我在第一篇文章中有涉及,只是那个时候我把三个监听方法里的具体实现给隐藏了,现在终于把它展现出来了。 ## deviceChanged方法 该方法会在设备连接的时候调用,但是我们做了个判断,就是设备状态的改变,那就说明该设备连接前`ADB`已经知道了该设备的存在,只是它与之前的状态发生了变化,所以在设备第一次连接的时候,该方法里的代码块是不会被调用的。那么再具体说说操作步骤: `1`.首先判断设备的改变是否是状态的改变,因为设备的变化很有多种,我们需要关心的设备于PC连接状态的改变,所以需要做判断。 `2`.然后从`mAllocatedDeviceMap`集合中尝试获得发生改变的设备,这一步是为了检查是否会影响到执行任务的设备。因为在任务执行过程中,不是每时每刻都能去检测状态,所以cts采用的是被动判断,如果状态发生改变,需要通知正在执行任务的设备监听器,由监听器去做相应的处理。 `3`.如果确定状态发生改变的设备是正在执行任务的设备,就需要将其状态设置为新状态。 `4`.如果不是正在执行任务的设备,那么再去验证是否属于另外2个集合中的设备。 `5`.如果是检测集合`mCheckDeviceMap`中的元素,那么也要重新设置设备状态,不过这个时候是从检测设备集合中得到设备再改变它的状态。 `6`.如果改变的设备既不是执行任务的设备,也不是检测中的设备,这个时候我们要判断是否是新设备,这个时候可能有人会有疑问?为什么不判断是否存在于可分配设备中呢?这得从`deviceChanged`的调用机制来说,`deviceChanged`只在设备连接进来的时候会调用,设备掉线的时候该方法不会被调用,那么自然这个里面无需判断是否是可分配设备中。只需要判断传进来的`IDevice`的状态是否在线且不在可分配设备列表中,这个时候就要按新设备来处理啦。另外判断是否在该集合中是需要在`deviceDisconnected`中判断的。 ## deviceConnected方法 这个方法会在有设备连接的时候调用,不管是新设备还是旧设备。它的处理步骤如下: `1`.首先判断该设备是否正在执行任务,如果正在执行任务,`ok`!我们需要更新该设备的状态。如果不是进入第二步: `2`.如果在线且通过判断其`SN`号是可用的,如果条件不通过,跳到第三步,如果通过的话,这个时候需要进行操作判断是否可以放到可用设备集合中。 `3`.是否存在于检测设备集合中,如果存在就将其设备更新下。 ## deviceDisconnected方法 这个方法会在设备离线的时候调用, `1`.首先试着从可用集合中删除该设备,然后进入第二步。 `2`.剩下的处理方式和上面的`deviceConnected`一样。 # 总结 设备管理的复杂性较之我所讲的,我所能说的也只是皮毛,希望对此有兴趣研究研究源码。提示一下,在研究源码之前先了解一下23种设计模式中常用模式,对你的理解会很有帮助。
';

(22)-自动检测设备

最后更新于:2022-04-01 06:55:10

# 感慨 经过三个月的蹉跎,项目终于可以推出1.0版本。中间经历过很多坑,中途我们的主程离职走了,我硬着头皮接替了他的工作,从cts框架的启动开始,一点一点地研究源码,debug来debug去,一点一点的理解其中的思想,到现在已经能在cts的框架的基础上做二次开发,能简单的认识到cts处理方式。很有幸我一进入自动化领域首先认识的是cts这套框架,随着研究的深入越来越佩服开发这套框架的google工程师们。我想说的是,做自动化框架开发的人都应该好好研究这个框架,肯定会受益匪浅。其实在学习的时候,我就已经写过好几篇文章,我也将其整理成合集,放到了[testerhome](http://testerhome.com/topics/1808)上。但那个时候毕竟还是懵懂时期,也没有跳出框架从全局来考虑,现在刚好有点时间,慢慢的把这几个月的研究成果总结一下。 # 设备管理的重要性 做Android自动化工具开发的都了解,你首先要解决的问题是设备管理问题,在支持 Mult Device的工具中尤其重要。新设备的加入、已有设备的断线离线,在执行case的过程中遇到设备离线了如何去恢复等等,都是在设备管理范畴之内的。那么cts是如何做到的? ## 1.包裹ADB ~~~ package com.android.tradefed.device; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.IDevice; /** * A wrapper that directs {@link IAndroidDebugBridge} calls to the 'real' * {@link AndroidDebugBridge}. */ class AndroidDebugBridgeWrapper implements IAndroidDebugBridge { private AndroidDebugBridge mAdbBridge = null; /** * Creates a {@link AndroidDebugBridgeWrapper}. */ AndroidDebugBridgeWrapper() { } /** * {@inheritDoc} */ @Override public IDevice[] getDevices() { if (mAdbBridge == null) { throw new IllegalStateException("getDevices called before init"); } return mAdbBridge.getDevices(); } /** * {@inheritDoc} */ @Override public void addDeviceChangeListener(IDeviceChangeListener listener) { AndroidDebugBridge.addDeviceChangeListener(listener); } /** * {@inheritDoc} */ @Override public void removeDeviceChangeListener(IDeviceChangeListener listener) { AndroidDebugBridge.removeDeviceChangeListener(listener); } /** * {@inheritDoc} */ @Override public void init(boolean clientSupport, String adbOsLocation) { AndroidDebugBridge.init(clientSupport); mAdbBridge = AndroidDebugBridge.createBridge(adbOsLocation, false); } /** * {@inheritDoc} */ @Override public void terminate() { AndroidDebugBridge.terminate(); } /** * {@inheritDoc} */ @Override public void disconnectBridge() { AndroidDebugBridge.disconnectBridge(); } } ~~~ 这里实际上用到了代理模式。cts中自定义的类`AndroidDebugBridgeWrapper`包裹了`AndroidDebugBridge`,我们只需要和`AndroidDebugBridgeWrapper`交互就行了。然后在`AndroidDebugBridge`的基础上自定义了一些方法。继承的方法中重要的是`addDeviceChangeListener`和`removeDeviceChangeListener`这两个方法,待会我们就要用到。 ## 2.启动ADB ~~~ public class DeviceManager implements IDeviceManager { ...... private IAndroidDebugBridge mAdbBridge; private ManagedDeviceListener mManagedDeviceListener; ...... /** * The DeviceManager should be retrieved from the {@link GlobalConfiguration} */ public DeviceManager() { } /** * Initialize the device manager. This must be called once and only once before any other * methods are called. */ synchronized void init(IDeviceSelection globalDeviceFilter, List<IDeviceMonitor> globalDeviceMonitors, IManagedTestDeviceFactory deviceFactory) { ...... mAdbBridge = createAdbBridge(); mManagedDeviceListener = new ManagedDeviceListener(); ...... mAdbBridge.addDeviceChangeListener(mManagedDeviceListener); ...... mAdbBridge.init(false /* client support */, "adb"); ...... } /** * Creates the {@link IAndroidDebugBridge} to use. * <p/> * Exposed so tests can mock this. * @returns the {@link IAndroidDebugBridge} */ synchronized IAndroidDebugBridge createAdbBridge() { return new AndroidDebugBridgeWrapper(); } } ~~~ 在`DeviceManage`类的`init`方法中,首先通过`createAdbBridge()`方法创建一个`IAndroidDebugBridge`对象,其实质是刚才定义的`AndroidDebugBridgeWrapper`对象。这样的话我们就得到了该对象的一个实例,接着我们调用了该实例的`init`方法(其实有2行代码我故意忽略了,后面会隆重登场),这样`ADB`的初始化工作就完成了。 ## 3.状态监听器 ~~~ private class ManagedDeviceListener implements IDeviceChangeListener { /** * {@inheritDoc} */ @Override public void deviceChanged(IDevice idevice, int changeMask) { ...... } /** * {@inheritDoc} */ @Override public void deviceConnected(IDevice idevice) { ...... } /** * {@inheritDoc} */ @Override public void deviceDisconnected(IDevice disconnectedDevice) { ...... } } ~~~ 在`DeviceManager`类中定义了一个私有类`ManagedDeviceListener`,该类实现了`ADB`中的接口`IDeviceChangeListener`。该接口实际上是观察者模式中的一个抽象观察者,我们定义的`ManagedDeviceListener`类是一个具体观察者。当我们注册为设备状态的观察者后,设备状态发生变化后,我们会被通知到。这个时候我们隆重请出刚才我们忽略的2行代码: ~~~ mManagedDeviceListener = new ManagedDeviceListener(); ...... mAdbBridge.addDeviceChangeListener(mManagedDeviceListener); ~~~ 这两行代码首先初始化了一个设备状态的具体观察者对象的实例,然后将其添加到通知列表中,这个时候`ADB`设备发生改变后,就会通知我们的对象,其中相应的三个方法`deviceChanged`,`deviceConnected`,`deviceDisconnected`会被调用,这个时候我们就可以通过一些处理得到新加入的设备,或者已有设备中离线的设备,然后将其删除。这样我们就能很好的监听着设备状态的改变。 ## 4.得到设备 既然我们能准确的监听着设备状态的改变,我们就要用一个(或许是多个)容器去保存这些设备。具体的操作流程我觉得单独写一篇文章来讲比较好,这样才能对得起它良好的设计。
';

(21)-测试结果收集系统

最后更新于:2022-04-01 06:55:07

# 1.UML图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddeec39a.jpg) # 2.解释 cts框架中将case的执行和case结果的收集分成了独立的2个部分,我们称case的结果的收集叫测试结果收集器。设计的思路来源为ddmlib中的ITestRunListener接口,该接口是一个抽象的观察者组件,cts创建有很多的具体的组件。但是这些组件不是一个一个去注册成为监听者的,而是在run的时候传递给ddmlib中RemoteAndroidTestRunner类的。所以只要我们继承了该接口,在run的时候把该接口的实例对象传过去,我们就能在对象实例的相应方法中接受信息。但是我们想要有多个地方都能接受到测试结果呢?没关系,cts为我们准备了ResultForwarder类。看具体实现细节: # 3.ResultForwarder ~~~ public class ResultForwarder implements ITestInvocationListener { private List<ITestInvocationListener> mListeners; /** * Create a {@link ResultForwarder} with deferred listener setting. Intended * only for use by subclasses. */ protected ResultForwarder() { mListeners = Collections.emptyList(); } /** * Create a {@link ResultForwarder}. * * @param listeners * the real {@link ITestInvocationListener}s to forward results * to */ public ResultForwarder(List<ITestInvocationListener> listeners) { mListeners = listeners; } /** * Alternate variable arg constructor for {@link ResultForwarder}. * * @param listeners * the real {@link ITestInvocationListener}s to forward results * to */ public ResultForwarder(ITestInvocationListener... listeners) { mListeners = Arrays.asList(listeners); } /** * Set the listeners after construction. Intended only for use by * subclasses. * * @param listeners * the real {@link ITestInvocationListener}s to forward results * to */ protected void setListeners(List<ITestInvocationListener> listeners) { mListeners = listeners; } /** * Set the listeners after construction. Intended only for use by * subclasses. * * @param listeners * the real {@link ITestInvocationListener}s to forward results * to */ protected void setListeners(ITestInvocationListener... listeners) { mListeners = Arrays.asList(listeners); } /** * {@inheritDoc} */ @Override public void invocationStarted(IBuildInfo buildInfo) { for (ITestInvocationListener listener : mListeners) { listener.invocationStarted(buildInfo); } } /** * {@inheritDoc} */ @Override public void invocationFailed(Throwable cause) { for (ITestInvocationListener listener : mListeners) { listener.invocationFailed(cause); } } /** * {@inheritDoc} */ @Override public void invocationEnded(long elapsedTime) { InvocationSummaryHelper.reportInvocationEnded(mListeners, elapsedTime); } /** * {@inheritDoc} */ @Override public TestSummary getSummary() { // should never be called return null; } /** * {@inheritDoc} */ @Override public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testLog] dataName: %s", dataName)); for (ITestInvocationListener listener : mListeners) { listener.testLog(dataName, dataType, dataStream); } } /** * {@inheritDoc}case开始时显式的调用 */ @Override public void testRunStarted(String runName, int testCount) { // CLog.logAndDisplay(LogLevel.INFO, String // .format("[testRunStarted] runName: %s testCount:%d", runName, // testCount)); for (ITestInvocationListener listener : mListeners) { listener.testRunStarted(runName, testCount); } } /** * {@inheritDoc} */ @Override public void testRunFailed(String errorMessage) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testRunFailed] errorMessage: %s", errorMessage)); for (ITestInvocationListener listener : mListeners) { listener.testRunFailed(errorMessage); } } /** * {@inheritDoc} */ @Override public void testRunStopped(long elapsedTime) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testRunStopped] elapsedTime: %d", elapsedTime)); for (ITestInvocationListener listener : mListeners) { listener.testRunStopped(elapsedTime); } } /** * {@inheritDoc} */ @Override public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testRunEnded] elapsedTime: %d", elapsedTime)); for (ITestInvocationListener listener : mListeners) { listener.testRunEnded(elapsedTime, runMetrics); } } /** * {@inheritDoc} */ @Override public void testStarted(TestIdentifier test) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testStarted] %s", test.toString())); for (ITestInvocationListener listener : mListeners) { listener.testStarted(test); } } /** * {@inheritDoc} */ @Override public void testFailed(TestFailure status, TestIdentifier test, String trace) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testFailed] %s", test.toString())); for (ITestInvocationListener listener : mListeners) { listener.testFailed(status, test, trace); } } /** * {@inheritDoc} */ @Override public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testEnded] %s", test.toString())); for (ITestInvocationListener listener : mListeners) { listener.testEnded(test, testMetrics); } } } ~~~ 该类中继承于ITestInvocationListener接口,ITestInvocationListener也继承与ITestRunListener接口,然后在ITestRunListener接口原有的方法上添加了一些cts所要用到的方法。而我们的ResultForwarder就是一个ITestInvocationListener接口,自然而然也是ITestRunListener对象。 而我们具体看看ResultForwarder构造方法你会发现,参数是多个ITestInvocationListener对象,传递进来保存到定义的集合中。这样当我们ResultForwarder传递到ddmlib内部的时候,ResultForwarder相应方法被调用后,里面的结果监听器集合里的每一个结果监听器都会收到结果。这样的设计难道不是很牛逼,我是觉得很牛逼啊!!! # 4.ResultForwarder的子类 cts还给测试时配备了3个使用型的失败现场证据收集器,请看下面的三个类定义。 ~~~ /** * 保存bugreport日志 A {@link ResultForwarder} that will forward a bugreport on * each failed test. */ private static class FailedTestBugreportGenerator extends ResultForwarder { private ITestDevice mDevice; public FailedTestBugreportGenerator(ITestInvocationListener listener, ITestDevice device) { super(listener); mDevice = device; } // 当ddmlib中ITestRunListener监听器触发testFailed方法时,该方法被调用.ITestRunListener的方法是由ddmlib内部调用的 // 该方法将在android-cts\repository\logs生成bugreport信息的文本文件并打包 @Override public void testFailed(TestFailure status, TestIdentifier test, String trace) { super.testFailed(status, test, trace); InputStreamSource bugSource = mDevice.getBugreport(); // System.out.println("CtsTest.FailedTestBugreportGenerator.testFailed: 执行case失败,需要保存当前bugreport的信息"); // CLog.logAndDisplay(LogLevel.INFO, // String.format("[testFailed] %s", test.toString())); super.testLog(String.format("bug-%s_%s", test.getClassName(), test.getTestName()), LogDataType.TEXT, bugSource); bugSource.cancel(); } } /** * 保存system缓冲区的日志 A {@link ResultForwarder} that will forward a logcat * snapshot on each failed test. */ private static class FailedTestLogcatGenerator extends ResultForwarder { private ITestDevice mDevice; private int mNumLogcatBytes; public FailedTestLogcatGenerator(ITestInvocationListener listener, ITestDevice device, int maxLogcatBytes) { super(listener); mDevice = device; mNumLogcatBytes = maxLogcatBytes; } @Override public void testFailed(TestFailure status, TestIdentifier test, String trace) { super.testFailed(status, test, trace); // sleep a small amount of time to ensure test failure stack trace // makes it into logcat // capture RunUtil.getDefault().sleep(10); InputStreamSource logSource = mDevice.getLogcat(mNumLogcatBytes); super.testLog(String.format("logcat-%s_%s", test.getClassName(), test.getTestName()), LogDataType.TEXT, logSource); logSource.cancel(); } } /** * 保存截图 A {@link ResultForwarder} that will forward a screenshot on test * failures. */ private static class FailedTestScreenshotGenerator extends ResultForwarder { private ITestDevice mDevice; public FailedTestScreenshotGenerator(ITestInvocationListener listener, ITestDevice device) { super(listener); mDevice = device; } @Override public void testFailed(TestFailure status, TestIdentifier test, String trace) { super.testFailed(status, test, trace); try { InputStreamSource screenSource = mDevice.getScreenshot(); super.testLog(String.format("screenshot-%s_%s", test.getClassName(), test.getTestName()), LogDataType.PNG, screenSource); screenSource.cancel(); } catch (DeviceNotAvailableException e) { // TODO: rethrow this somehow CLog.e("Device %s became unavailable while capturing screenshot, %s", mDevice.getSerialNumber(), e.toString()); } } } ~~~ 老大:bugreport信息保存 老二:logcat信息保存 老三:现场截图
';

(20)-cts自身log系统

最后更新于:2022-04-01 06:55:05

# Log系统 ## log系统中类图 在cts的log包中。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddd5cd21.jpg) ## log系统的入口 入口类为CLog。采用的是代理模式,被代理的类是DDM内部的Log类。 ### CLog ~~~ public static class CLog { /** * The shim version of {@link Log#v(String, String)}. * * @param message The {@code String} to log */ public static void v(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.v(getClassName(2), message); } /** * The shim version of {@link Log#v(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void v(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.v(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#d(String, String)}. * * @param message The {@code String} to log */ public static void d(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.d(getClassName(2), message); } /** * The shim version of {@link Log#d(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void d(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.d(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#i(String, String)}. * * @param message The {@code String} to log */ public static void i(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.i(getClassName(2), message); } /** * The shim version of {@link Log#i(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void i(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.i(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#w(String, String)}. * * @param message The {@code String} to log */ public static void w(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.w(getClassName(2), message); } /** * The shim version of {@link Log#w(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void w(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.w(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#e(String, String)}. * * @param message The {@code String} to log */ public static void e(String message) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), message); } /** * The shim version of {@link Log#e(String, String)}. Also calls String.format for * convenience. * * @param format A format string for the message to log * @param args The format string arguments */ public static void e(String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), String.format(format, args)); } /** * The shim version of {@link Log#e(String, Throwable)}. * * @param t the {@link Throwable} to output. */ public static void e(Throwable t) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.e(getClassName(2), t); } /** * The shim version of {@link Log#logAndDisplay(LogLevel, String, String)}. * * @param logLevel the {@link LogLevel} * @param format A format string for the message to log * @param args The format string arguments */ public static void logAndDisplay(LogLevel logLevel, String format, Object... args) { // frame 2: skip frames 0 (#getClassName) and 1 (this method) Log.logAndDisplay(logLevel, getClassName(2), String.format(format, args)); } /** * Return the simple classname from the {@code frame}th stack frame in the call path. * Note: this method does <emph>not</emph> check array bounds for the stack trace length. * * @param frame The index of the stack trace frame to inspect for the class name * @return The simple class name (or full-qualified if an error occurs getting a ref to the * class) for the given element of the stack trace. */ public static String getClassName(int frame) { StackTraceElement[] frames = (new Throwable()).getStackTrace(); String fullName = frames[frame].getClassName(); @SuppressWarnings("rawtypes") Class klass = null; try { klass = Class.forName(fullName); } catch (ClassNotFoundException e) { // oops; not much we can do. // Intentionally fall through so we hit the null check below } if (klass == null) { return fullName; } else { return klass.getSimpleName(); } } } ~~~ 所以CLog里的方法也是直接调用Log相应的方法。 仔细看看Log类中相应的log方法,比如Log.i()、Log.e()。他们都不约而同的转向了println方法。 ~~~ private static void println(LogLevel logLevel, String tag, String message) { if (logLevel.getPriority() >= mLevel.getPriority()) { if (sLogOutput != null) { sLogOutput.printLog(logLevel, tag, message); } else { printLog(logLevel, tag, message); } } } ~~~ 其中sLogOutput为Log类放出的接口对象,接口定义如下所示。一般情况下如果你不设置该sLogOutput值的话,它为null。Log信息的打印会直接转到System.out.println,直接在控制台输出信息。但是如果我们实现该接口,将该属性附上值,我们就可重定向log的输出,转而到我们自己定义的输出设备中。所以Cts的log系统就是利用这一点做的。 ### ILogOutput ~~~ public interface ILogOutput { /** * Sent when a log message needs to be printed. * @param logLevel The {@link LogLevel} enum representing the priority of the message. * @param tag The tag associated with the message. * @param message The message to display. */ public void printLog(LogLevel logLevel, String tag, String message); /** * Sent when a log message needs to be printed, and, if possible, displayed to the user * in a dialog box. * @param logLevel The {@link LogLevel} enum representing the priority of the message. * @param tag The tag associated with the message. * @param message The message to display. */ public void printAndPromptLog(LogLevel logLevel, String tag, String message); } private static ILogOutput sLogOutput; ~~~ 上面的ILogOutput接口只有2个方法,Cts扩展了该接口,重新定义了一个接口,继承ILogOutput接口,取名为ILeveledLogOutput。该接口主要定义输出设备类要使用的方法。 ### ILeveledLogOutput ~~~ public interface ILeveledLogOutput extends ILogOutput { /** * Initialize the log, creating any required IO resources. */ public void init() throws IOException; /** * Gets the minimum log level to display. * * @return the current {@link LogLevel} */ public LogLevel getLogLevel(); /** * Sets the minimum log level to display. * * @param logLevel the {@link LogLevel} to display */ public void setLogLevel(LogLevel logLevel); /** * Grabs a snapshot stream of the log data. * <p/> * Must not be called after {@link ILeveledLogOutput#closeLog()}. * <p/> * The returned stream is not guaranteed to have optimal performance. Callers may wish to * wrap result in a {@link BufferedInputStream}. * * @return a {@link InputStreamSource} of the log data * @throws IllegalStateException if called when log has been closed. */ public InputStreamSource getLog(); /** * Closes the log and performs any cleanup before closing, as necessary. */ public void closeLog(); /** * @return a {@link ILeveledLogOutput} */ public ILeveledLogOutput clone(); } ~~~ ILeveledLogOutput拥有2个实现类 ### FileLogger ~~~ public class FileLogger implements ILeveledLogOutput { private static final String TEMP_FILE_PREFIX = "tradefed_log_"; private static final String TEMP_FILE_SUFFIX = ".txt"; @Option(name = "log-level", description = "the minimum log level to log.") private LogLevel mLogLevel = LogLevel.DEBUG; @Option(name = "log-level-display", shortName = 'l', description = "the minimum log level to display on stdout.", importance = Importance.ALWAYS) private LogLevel mLogLevelDisplay = LogLevel.ERROR; @Option(name = "log-tag-display", description = "Always display given tags logs on stdout") private Collection<String> mLogTagsDisplay = new HashSet<String>(); @Option(name = "max-log-size", description = "maximum allowable size of tmp log data in mB.") private long mMaxLogSizeMbytes = 20; private SizeLimitedOutputStream mLogStream; /** * Adds tags to the log-tag-display list * * @param tags collection of tags to add */ void addLogTagsDisplay(Collection<String> tags) { mLogTagsDisplay.addAll(tags); } public FileLogger() { } /** * {@inheritDoc} */ @Override public void init() throws IOException { mLogStream = new SizeLimitedOutputStream(mMaxLogSizeMbytes * 1024 * 1024, TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX); } /** * Creates a new {@link FileLogger} with the same log level settings as the current object. * <p/> * Does not copy underlying log file content (ie the clone's log data will be written to a new * file.) */ @Override public ILeveledLogOutput clone() { FileLogger logger = new FileLogger(); logger.setLogLevelDisplay(mLogLevelDisplay); logger.setLogLevel(mLogLevel); logger.addLogTagsDisplay(mLogTagsDisplay); return logger; } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { internalPrintLog(logLevel, tag, message, true /* force print to stdout */); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { internalPrintLog(logLevel, tag, message, false /* don't force stdout */); } /** * A version of printLog(...) which can be forced to print to stdout, even if the log level * isn't above the urgency threshold. */ private void internalPrintLog(LogLevel logLevel, String tag, String message, boolean forceStdout) { String outMessage = LogUtil.getLogFormatString(logLevel, tag, message); if (forceStdout || logLevel.getPriority() >= mLogLevelDisplay.getPriority() || mLogTagsDisplay.contains(tag)) { System.out.print(outMessage); } try { writeToLog(outMessage); } catch (IOException e) { e.printStackTrace(); } } /** * Writes given message to log. * <p/> * Exposed for unit testing. * * @param outMessage the entry to write to log * @throws IOException */ void writeToLog(String outMessage) throws IOException { if (mLogStream != null) { mLogStream.write(outMessage.getBytes()); } } /** * {@inheritDoc} */ @Override public LogLevel getLogLevel() { return mLogLevel; } /** * {@inheritDoc} */ @Override public void setLogLevel(LogLevel logLevel) { mLogLevel = logLevel; } /** * Sets the log level filtering for stdout. * * @param logLevel the minimum {@link LogLevel} to display */ void setLogLevelDisplay(LogLevel logLevel) { mLogLevelDisplay = logLevel; } /** * Gets the log level filtering for stdout. * * @return the current {@link LogLevel} */ LogLevel getLogLevelDisplay() { return mLogLevelDisplay; } /** * {@inheritDoc} */ @Override public InputStreamSource getLog() { if (mLogStream != null) { try { // create a InputStream from log file mLogStream.flush(); return new SnapshotInputStreamSource(mLogStream.getData()); } catch (IOException e) { System.err.println("Failed to get log"); e.printStackTrace(); } } return new ByteArrayInputStreamSource(new byte[0]); } /** * {@inheritDoc} */ @Override public void closeLog() { try { doCloseLog(); } catch (IOException e) { e.printStackTrace(); } } /** * Flushes stream and closes log file. * <p/> * Exposed for unit testing. * * @throws IOException */ void doCloseLog() throws IOException { SizeLimitedOutputStream stream = mLogStream; mLogStream = null; if (stream != null) { try { stream.flush(); stream.close(); } finally { stream.delete(); } } } /** * Dump the contents of the input stream to this log * * @param createInputStream * @throws IOException */ void dumpToLog(InputStream inputStream) throws IOException { if (mLogStream != null) { StreamUtil.copyStreams(inputStream, mLogStream); } } } ~~~ ### StdoutLogger ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.config.OptionClass; import com.android.tradefed.result.ByteArrayInputStreamSource; import com.android.tradefed.result.InputStreamSource; import java.io.IOException; /** * A {@link ILeveledLogOutput} that directs log messages to stdout. */ @OptionClass(alias = "stdout") public class StdoutLogger implements ILeveledLogOutput { @Option(name="log-level", description="minimum log level to display.", importance = Importance.ALWAYS) private LogLevel mLogLevel = LogLevel.INFO; //StdoutLogger(){} /** * {@inheritDoc} * * */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { printLog(logLevel, tag, message); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { LogUtil.printLog(logLevel, tag, message); } /** * {@inheritDoc} */ @Override public void setLogLevel(LogLevel logLevel) { mLogLevel = logLevel; } /** * {@inheritDoc} */ @Override public LogLevel getLogLevel() { return mLogLevel; } /** * {@inheritDoc} */ @Override public void closeLog() { // ignore } /** * {@inheritDoc} */ @Override public InputStreamSource getLog() { // not supported - return empty stream return new ByteArrayInputStreamSource(new byte[0]); } @Override public ILeveledLogOutput clone() { return new StdoutLogger(); } @Override public void init() throws IOException { // ignore } } ~~~ 又定义了一个Log注册器的接口,叫ILogRegistry。该接口也是继承ILogOutput,但是该接口中的方法明显不是一个log输出系统,而是一个注册log所要用到的方法。 ### ILogRegistry ~~~ public interface ILogRegistry extends ILogOutput { /** * Set the log level display for the global log * * @param logLevel the {@link LogLevel} to use */ public void setGlobalLogDisplayLevel(LogLevel logLevel); /** * Set the log tags to display for the global log */ public void setGlobalLogTagDisplay(Collection<String> logTagsDisplay); /** * Returns current log level display for the global log * * @return logLevel the {@link LogLevel} to use */ public LogLevel getGlobalLogDisplayLevel(); /** * Registers the logger as the instance to use for the current thread. */ public void registerLogger(ILeveledLogOutput log); /** * Unregisters the current logger in effect for the current thread. */ public void unregisterLogger(); /** * Dumps the entire contents of a {@link ILeveledLogOutput} logger to the global log. * <p/> * This is useful in scenarios where you know the logger's output won't be saved permanently, * yet you want the contents to be saved somewhere and not lost. * * @param log */ public void dumpToGlobalLog(ILeveledLogOutput log); /** * Closes and removes all logs being managed by this LogRegistry. */ public void closeAndRemoveAllLogs(); /** * Diagnosis method to dump all logs to files. */ public void dumpLogs(); } ~~~ ILogRegistry实现类,该类采用的是单例模式 ### LogRegistry ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.log; import com.android.ddmlib.Log; import com.android.ddmlib.Log.LogLevel; import com.android.tradefed.config.Option; import com.android.tradefed.config.OptionClass; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.StreamUtil; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; /** * A {@link ILogRegistry} implementation that multiplexes and manages different loggers, * using the appropriate one based on the {@link ThreadGroup} of the thread making the call. * <p/> * Note that the registry hashes on the ThreadGroup in which a thread belongs. If a thread is * spawned with its own explicitly-supplied ThreadGroup, it will not inherit the parent thread's * logger, and thus will need to register its own logger with the LogRegistry if it wants to log * output. */ @OptionClass(alias="logreg") public class LogRegistry implements ILogRegistry { private static final String LOG_TAG = "LogRegistry"; private static LogRegistry mLogRegistry = null; private Map<ThreadGroup, ILeveledLogOutput> mLogTable = new Hashtable<ThreadGroup, ILeveledLogOutput>(); private FileLogger mGlobalLogger; /** * Package-private constructor; callers should use {@link #getLogRegistry} to get an instance of * the {@link LogRegistry}. */ LogRegistry() { try { mGlobalLogger = new FileLogger(); mGlobalLogger.init(); } catch (IOException e) { System.err.println("Failed to create global logger"); throw new IllegalStateException(e); } } /** * Get the {@link LogRegistry} instance * <p/> * * @return a {@link LogRegistry} that can be used to register, get, write to, and close logs */ public static ILogRegistry getLogRegistry() { if (mLogRegistry == null) { mLogRegistry = new LogRegistry(); } return mLogRegistry; } /** * {@inheritDoc} */ @Override public void setGlobalLogDisplayLevel(LogLevel logLevel) { mGlobalLogger.setLogLevelDisplay(logLevel); } /** * {@inheritDoc} */ @Override public void setGlobalLogTagDisplay(Collection<String> logTagsDisplay) { mGlobalLogger.addLogTagsDisplay(logTagsDisplay); } /** * {@inheritDoc} */ @Override public LogLevel getGlobalLogDisplayLevel() { return mGlobalLogger.getLogLevelDisplay(); } /** * {@inheritDoc} */ @Override public void registerLogger(ILeveledLogOutput log) { ILeveledLogOutput oldValue = mLogTable.put(getCurrentThreadGroup(), log); if (oldValue != null) { Log.e(LOG_TAG, "Registering a new logger when one already exists for this thread!"); oldValue.closeLog(); } } /** * {@inheritDoc} */ @Override public void unregisterLogger() { ThreadGroup currentThreadGroup = getCurrentThreadGroup(); if (currentThreadGroup != null) { mLogTable.remove(currentThreadGroup); } else { printLog(LogLevel.ERROR, LOG_TAG, "Unregistering when thread has no logger registered."); } } /** * {@inheritDoc} */ @Override public void dumpToGlobalLog(ILeveledLogOutput log) { InputStreamSource source = log.getLog(); try { InputStream stream = source.createInputStream(); mGlobalLogger.dumpToLog(stream); StreamUtil.close(stream); } catch (IOException e) { System.err.println("Failed to dump log"); e.printStackTrace(); } finally { source.cancel(); } } /** * Gets the current thread Group. * <p/> * Exposed so unit tests can mock * * @return the ThreadGroup that the current thread belongs to */ ThreadGroup getCurrentThreadGroup() { return Thread.currentThread().getThreadGroup(); } /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { ILeveledLogOutput log = getLogger(); LogLevel currentLogLevel = log.getLogLevel(); if (logLevel.getPriority() >= currentLogLevel.getPriority()) { log.printLog(logLevel, tag, message); } } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { getLogger().printAndPromptLog(logLevel, tag, message); } /** * Gets the underlying logger associated with this thread. * * @return the logger for this thread, or null if one has not been registered. */ ILeveledLogOutput getLogger() { ILeveledLogOutput log = mLogTable.get(getCurrentThreadGroup()); if (log == null) { // If there's no logger set for this thread, use global logger log = mGlobalLogger; } return log; } /** * {@inheritDoc} */ @Override public void closeAndRemoveAllLogs() { Collection<ILeveledLogOutput> allLogs = mLogTable.values(); Iterator<ILeveledLogOutput> iter = allLogs.iterator(); while (iter.hasNext()) { ILeveledLogOutput log = iter.next(); log.closeLog(); iter.remove(); } saveGlobalLog(); mGlobalLogger.closeLog(); } /** * {@inheritDoc} */ private void saveGlobalLog() { InputStreamSource globalLog = mGlobalLogger.getLog(); saveLog("tradefed_global_log_", globalLog); globalLog.cancel(); } /** * Save log data to a temporary file * * @param filePrefix the file name prefix * @param logData the textual log data */ private void saveLog(String filePrefix, InputStreamSource logData) { try { File tradefedLog = FileUtil.createTempFile(filePrefix, ".txt"); FileUtil.writeToFile(logData.createInputStream(), tradefedLog); CLog.logAndDisplay(LogLevel.INFO, String.format("Saved log to %s", tradefedLog.getAbsolutePath())); } catch (IOException e) { // ignore } } /** * {@inheritDoc} */ @Override public void dumpLogs() { for (Map.Entry<ThreadGroup, ILeveledLogOutput> logEntry : mLogTable.entrySet()) { // use thread group name as file name - assume its descriptive String filePrefix = String.format("%s_log_", logEntry.getKey().getName()); InputStreamSource logSource = logEntry.getValue().getLog(); saveLog(filePrefix, logSource); logSource.cancel(); } // save global log last saveGlobalLog(); } } ~~~ ## Log系统的运行过程 当cts系统开始运行的时候,我们会将LogRegistry对象注册为Log类中sLogOutput,将DDM内部的Log输出重定向到CTS自己的Log系统中,该对象就是LogRegistry对象,但是该对象不是直接输出lgo的,它照样采用代理模式或者也可以说是装饰者模式,负责调配、管理所有的Log设备。因为cts中的允许多任务同时运行,所以管理好每一个线程的log信息,很有必要。那具体是怎么做的呢,当Cts启动的时候,将Log注册器设置为DDM内部的log重定向设备,代码如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddd8355d.jpg) 在initLogging()方法中,调用Log.setLogOutput方法将我们的log注册器设置成sLogOutput属性。 ~~~ public static void setLogOutput(ILogOutput logOutput) { sLogOutput = logOutput; } ~~~ 那么以后所有调用CLog打印的信息的操作都会转到我们的LogRegistry对象中相应的方法中。那么就有必要来看看LogRegistry中对ILogOutput方法的实现 ~~~ /** * {@inheritDoc} */ @Override public void printLog(LogLevel logLevel, String tag, String message) { ILeveledLogOutput log = getLogger(); LogLevel currentLogLevel = log.getLogLevel(); if (logLevel.getPriority() >= currentLogLevel.getPriority()) { log.printLog(logLevel, tag, message); } } /** * {@inheritDoc} */ @Override public void printAndPromptLog(LogLevel logLevel, String tag, String message) { getLogger().printAndPromptLog(logLevel, tag, message); } ~~~ 这两个方法中,首先第一步都是要调用getLogger方法得到具体的输出设备。 ~~~ ILeveledLogOutput getLogger() { ILeveledLogOutput log = mLogTable.get(getCurrentThreadGroup()); if (log == null) { // If there's no logger set for this thread, use global logger log = mGlobalLogger; } return log; } ~~~ 由上面的代码可以看出,如果我们在Map对象中找不到log输出设备,我们就会使用全局log设备。那么就要先看看全局log器是啥,如果能从map中得到log器,那这个log器又是啥。 首先来看全局log器 ~~~ private FileLogger mGlobalLogger; ~~~ 它的定义是一个FileLogger对象:该对象是一个文件log系统,它会把CLog传入的信息在打印的同时也会保存到文件中。那么我们可以说这是个log文件。 那么如果Map不为空,我们能得到啥log器。追踪源码可以发现向map放入log器的代码存在于registerLogger方法中。那么该方法为何会将当前线程所在的线程组作为关键字放到map中。那我们就要看看谁调用了registerLogger方法。 ~~~ @Override public void registerLogger(ILeveledLogOutput log) { ILeveledLogOutput oldValue = mLogTable.put(getCurrentThreadGroup(), log); if (oldValue != null) { Log.e(LOG_TAG, "Registering a new logger when one already exists for this thread!"); oldValue.closeLog(); } } ~~~ 原来在TestInvocation类中的invoke方法中调用这个方法 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dddcc2b4.jpg) 如果你看过我之前的关于cts框架分析的文章,你应该会了解这个invoke在何时被调用,在每个任务启动的时候,会创建一个单独的线程来执行这个任务,这个时候invoke会被调用。那们我们也就明白为什么会用线程所在的线程组来当做Map的关键字啦。这样以后我们就可以通过调用者所在线程组得到器log输出设备。这样就区别了每个线程的log。
';

(19)-设备状态的分类以及恢复模式的分类

最后更新于:2022-04-01 06:55:03

# 设备状态 ## 类图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddd3d7a4.jpg) 枚举 : TestDeviceState,其实是adb中DeviceState扩展而来。 1.FASTBOOT:线刷状态(根据fastboot监听器获得经过设置) 2.ONLINE:在线状态(根据DeviceState值转化而来) 3.OFFLINE:离线状态(根据DeviceState值转化而来) 4.RECOVERY:卡刷状态(根据DeviceState值转化而来) 5.NOT_AVAILABLE:不可用状态(根据情况不同手动设置) 枚举:RecoveryMode,恢复模式。在进行设备恢复的时候,会先判断该设备的恢复模式。 1.NONE:该设备不进行设备恢复 2.ONLINE:该设备需要恢复到online状态 3.AVAILABLE:该设备需要恢复到可用状态 ## 理解 1.最需要理解是TestDeviceState.NOT_AVAILABLE状态: 一般情况下用adb devices没有获得该设备的任何状态,但是程序知道设备肯定是存在的。这个时候就可以判断该设备是处于NOT_AVAILABLE状态。 2.TestDeviceState.OFFLINE的状态和TestDeviceState.NOT_AVAILABLE的区别: OFFLINE是离线状态,但是这种离线adb devices是可以检测到的,这个时候设备是有反馈的。 但是NOT_AVAILABLE是adb devices无法得到的,这个时候压根就不理睬你。 比如QQ中的离线提醒和下线的区别,大家一下子就明白了。离线状态好比TestDeviceState.OFFLINE,有时候可能会给你恢复,提示该用户暂时不在线。 下线就好比TestDeviceState.NOT_AVAILABLE。 3.TestDeviceState.ONLINE和RecoveryMode.ONLINE区别: TestDeviceState.ONLINE是一种状态的分类,而RecoveryMode.ONLINE是在设备离线后,设备恢复要达到的一种目标的分类。当设备处于TestDeviceState.OFFLINE的时候或者TestDeviceState.NOT_AVAILABLE的时候,它就要调用ITestRecovery来恢复设备,那么RecoveryMode就定义了,该设备恢复的目标。ITestRecovery中的方法执行的时候,会先判断要恢复到什么状态。然后才会做相应的工作。 ## 代码 ### TestDeviceState ~~~ public enum TestDeviceState { FASTBOOT, ONLINE, OFFLINE, RECOVERY, NOT_AVAILABLE; /** * Converts from {@link TestDeviceState} to {@link DeviceState} * @return the {@link DeviceState} or <code>null</code> */ DeviceState getDdmsState() { switch (this) { case ONLINE: return DeviceState.ONLINE; case OFFLINE: return DeviceState.OFFLINE; case RECOVERY: return DeviceState.RECOVERY; default: return null; } } /** * Returns the {@link TestDeviceState} corresponding to the {@link DeviceState}. */ static TestDeviceState getStateByDdms(DeviceState ddmsState) { if (ddmsState == null) { return TestDeviceState.NOT_AVAILABLE; } switch (ddmsState) { case ONLINE: return TestDeviceState.ONLINE; case OFFLINE: return TestDeviceState.OFFLINE; case RECOVERY: return TestDeviceState.RECOVERY; } return TestDeviceState.NOT_AVAILABLE; } } ~~~ ### RecoveryMode ~~~ public enum RecoveryMode { /** don't attempt to recover device. */ NONE, /** recover device to online state only */ ONLINE, /** * Recover device into fully testable state - framework is up, and external storage is * mounted. */ AVAILABLE } ~~~ ## 重点理解 上面说了TestDeviceState.NOT_AVAILABLE很特殊,那么下面就来看看哪些场景下设备状态被设置成了NOT_AVAILABLE 1.DeviceManager.createTestDevice() ~~~ IManagedTestDevice createTestDevice(IDevice allocatedDevice, IDeviceStateMonitor monitor) { IManagedTestDevice testDevice = new TestDevice(allocatedDevice, monitor); testDevice.setFastbootEnabled(mFastbootEnabled); if (allocatedDevice instanceof FastbootDevice) { testDevice.setDeviceState(TestDeviceState.FASTBOOT); } else if (allocatedDevice instanceof StubDevice) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } return testDevice; } ~~~ 当设备属于虚拟设备的时候,也设置该设备为NOT_AVAILABLE状态。 2.DeviceManager的私有类ManagedDeviceListener.deviceDisconnected() ~~~ public void deviceDisconnected(IDevice disconnectedDevice) { if (mAvailableDeviceQueue.remove(disconnectedDevice)) { CLog.i("Removed disconnected device %s from available queue", disconnectedDevice.getSerialNumber()); } IManagedTestDevice testDevice = mAllocatedDeviceMap.get(disconnectedDevice.getSerialNumber()); if (testDevice != null) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } else if (mCheckDeviceMap.containsKey(disconnectedDevice.getSerialNumber())) { IDeviceStateMonitor monitor = mCheckDeviceMap.get(disconnectedDevice.getSerialNumber()); monitor.setState(TestDeviceState.NOT_AVAILABLE); } updateDeviceMonitor(); } ~~~ 当adb监听到有设备断线的时候,会判断该设备是否处于已分配或者已检测的设备列表中,则设置其状态为NOT_AVAILABLE。 3.DeviceManager的私有类FastbootMonitor.run() ~~~ synchronized (mAllocatedDeviceMap) { for (IManagedTestDevice testDevice : mAllocatedDeviceMap.values()) { if (!serials.contains(testDevice.getSerialNumber()) && testDevice.getDeviceState().equals(TestDeviceState.FASTBOOT)) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } } } ~~~ 当处于已分配(就是正在执行任务)的任务列表中的设备被检测出来处于fastboot状态,这个时候就要将设备状态设置成NOT_AVAILABLE。 所以说,NOT_AVAILABLE对于处于执行任务的设备来说,比较重要的一种状态。
';

(18)-设备恢复

最后更新于:2022-04-01 06:55:00

# 设备恢复 ## 类关系 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddd1b454.jpg) ## 理解 4个分类中,AbortRecovery和StubDeviceRecovery2个类都是直接继承方法,直接做报错处理。但是报错的信息体现了他们的不同点,一个是放弃恢复,一个是不能恢复。还有客观世界中的区别:好比在说,我能上清华,只是我不上而已。 那么就剩下另外2个类。ReconnectingRecovery和WaitDeviceRecovery。一个是重连,一个实等待设备恢复。2者有什么区别,去具体看方法里的定义吧。 ## 代码 AbortRecovery:放弃恢复。就是我不恢复 ~~~ private static class AbortRecovery implements IDeviceRecovery { /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } /** * {@inheritDoc} */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("aborted test session"); } } ~~~ StubDeviceRecovery:为虚拟设备定义的,意思是:我不能恢复。 ~~~ public class StubDeviceRecovery implements IDeviceRecovery { /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } /** * {@inheritDoc} */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } } ~~~ ReconnectingRecovery:重新连接设备。用于wifi连接设备的时候,如果短线了,调用adb connect的命令来连接设备。 ~~~ public class ReconnectingRecovery implements IDeviceRecovery { private static final int ADB_TIMEOUT = 2 * 60 * 1000; private static final int CONNECTION_ATTEMPTS = 5; /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { String serial = monitor.getSerialNumber(); // disconnect - many versions of adb client have stale TCP connection // status getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "disconnect", serial); // try to reconnect int attempt = 1; do { CLog.i("Trying to reconnect with device " + serial + " / attempt " + attempt); getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "connect", serial); } while (monitor.waitForDeviceOnline() == null && ++attempt <= CONNECTION_ATTEMPTS); String errMsg = "Could not recover device " + serial + " after " + --attempt + " attempts"; // occasionally device is erroneously reported as online - double check // that we can shell into device if (!monitor.waitForDeviceShell(10 * 1000)) { throw new DeviceUnresponsiveException(errMsg); } if (!recoverUntilOnline) { if (monitor.waitForDeviceAvailable() == null) { throw new DeviceUnresponsiveException(errMsg); } } CLog.v("Successfully reconnected with device " + serial); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new java.lang.UnsupportedOperationException( "This implementation can't recover a device in bootloader mode."); } /** * {@inheritDoc} * <p> * This implementation assumes devices in recovery mode can't be talked to * at all, so it will try to recover a device and leave it in fully booted * mode. */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { recoverDevice(monitor, false); } /** * Get the {@link RunUtil} instance to use. * <p/> * Exposed for unit testing. */ IRunUtil getRunUtil() { return RunUtil.getDefault(); } } ~~~ WaitDeviceRecovery,这才是真正要去理解的类。一般的测试主要是用的该类来恢复设备。 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.TimeoutException; import com.android.tradefed.config.Option; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import java.io.IOException; /** * A simple implementation of a {@link IDeviceRecovery} that waits for device to be online and * respond to simple commands. */ public class WaitDeviceRecovery implements IDeviceRecovery { private static final String LOG_TAG = "WaitDeviceRecovery"; /** the time in ms to wait before beginning recovery attempts */ protected static final long INITIAL_PAUSE_TIME = 5 * 1000; /** * The number of attempts to check if device is in bootloader. * <p/> * Exposed for unit testing */ public static final int BOOTLOADER_POLL_ATTEMPTS = 3; // TODO: add a separate configurable timeout per operation @Option(name="device-wait-time", description="maximum time in ms to wait for a single device recovery command.") protected long mWaitTime = 4 * 60 * 1000; @Option(name="bootloader-wait-time", description="maximum time in ms to wait for device to be in fastboot.") protected long mBootloaderWaitTime = 30 * 1000; @Option(name="shell-wait-time", description="maximum time in ms to wait for device shell to be responsive.") protected long mShellWaitTime = 30 * 1000; @Option(name = "disable-unresponsive-reboot", description = "If this is set, we will not attempt to reboot an unresponsive device" + "that is in userspace. Note that this will have no effect if the device is in " + "fastboot or is expected to be in fastboot.") protected boolean mDisableUnresponsiveReboot = false; /** * Get the {@link RunUtil} instance to use. * <p/> * Exposed for unit testing. */ protected IRunUtil getRunUtil() { return RunUtil.getDefault(); } /** * Sets the maximum time in ms to wait for a single device recovery command. */ void setWaitTime(long waitTime) { mWaitTime = waitTime; } /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { // device may have just gone offline // sleep a small amount to give ddms state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // ensure bootloader state is updated,最新获取过fastboot devices信息 monitor.waitForDeviceBootloaderStateUpdate(); if (monitor.getDeviceState().equals(TestDeviceState.FASTBOOT)) { Log.i(LOG_TAG, String.format( "Found device %s in fastboot but expected online. Rebooting...", monitor.getSerialNumber())); // TODO: retry if failed getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(), "reboot"); } // wait for device online IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } // occasionally device is erroneously reported as online - double check that we can shell // into device if (!monitor.waitForDeviceShell(mShellWaitTime)) { // treat this as a not available device handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } if (!recoverUntilOnline) { if (monitor.waitForDeviceAvailable(mWaitTime) == null) { // device is online but not responsive handleDeviceUnresponsive(device, monitor); } } } /** * Handle situation where device is online but unresponsive. * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceUnresponsive(IDevice device, IDeviceStateMonitor monitor) throws DeviceNotAvailableException { if (!mDisableUnresponsiveReboot) { rebootDevice(device); } IDevice newdevice = monitor.waitForDeviceOnline(); if (newdevice == null) { handleDeviceNotAvailable(monitor, false); return; } if (monitor.waitForDeviceAvailable(mWaitTime) == null) { throw new DeviceUnresponsiveException(String.format( "Device %s is online but unresponsive", monitor.getSerialNumber())); } } /** * Handle situation where device is not available. * * @param monitor the {@link IDeviceStateMonitor} * @param recoverTillOnline if true this method should return if device is online, and not * check for responsiveness * @throws DeviceNotAvailableException */ protected void handleDeviceNotAvailable(IDeviceStateMonitor monitor, boolean recoverTillOnline) throws DeviceNotAvailableException { throw new DeviceNotAvailableException(String.format("Could not find device %s", monitor.getSerialNumber())); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { // device may have just gone offline // wait a small amount to give device state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // poll and wait for device to return to valid state long pollTime = mBootloaderWaitTime / BOOTLOADER_POLL_ATTEMPTS; for (int i=0; i < BOOTLOADER_POLL_ATTEMPTS; i++) { if (monitor.waitForDeviceBootloader(pollTime)) { handleDeviceBootloaderUnresponsive(monitor); // passed above check, abort return; } else if (monitor.getDeviceState() == TestDeviceState.ONLINE) { handleDeviceOnlineExpectedBootloader(monitor); return; } } handleDeviceBootloaderNotAvailable(monitor); } /** * Handle condition where device is online, but should be in bootloader state. * <p/> * If this method * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceOnlineExpectedBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { Log.i(LOG_TAG, String.format("Found device %s online but expected fastboot.", monitor.getSerialNumber())); // call waitForDeviceOnline to get handle to IDevice IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceBootloaderNotAvailable(monitor); return; } rebootDeviceIntoBootloader(device); if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) { throw new DeviceNotAvailableException(String.format( "Device %s not in bootloader after reboot", monitor.getSerialNumber())); } } /** * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceBootloaderUnresponsive(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { CLog.i("Found device %s in fastboot but potentially unresponsive.", monitor.getSerialNumber()); // TODO: retry reboot getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(), "reboot-bootloader"); // wait for device to reboot monitor.waitForDeviceNotAvailable(20*1000); if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) { throw new DeviceNotAvailableException(String.format( "Device %s not in bootloader after reboot", monitor.getSerialNumber())); } } /** * Reboot device into bootloader. * * @param device the {@link IDevice} to reboot. */ protected void rebootDeviceIntoBootloader(IDevice device) { try { device.reboot("bootloader"); } catch (IOException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } catch (TimeoutException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber())); } catch (AdbCommandRejectedException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } } /** * Reboot device into bootloader. * * @param device the {@link IDevice} to reboot. */ protected void rebootDevice(IDevice device) { try { device.reboot(null); } catch (IOException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } catch (TimeoutException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber())); } catch (AdbCommandRejectedException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } } /** * Handle situation where device is not available when expected to be in bootloader. * * @param monitor the {@link IDeviceStateMonitor} * @throws DeviceNotAvailableException */ protected void handleDeviceBootloaderNotAvailable(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException(String.format( "Could not find device %s in bootloader", monitor.getSerialNumber())); } /** * {@inheritDoc} */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } } ~~~ ## 重点 现在着重来理解WaitDeviceRecovery中的方法。 ### recoverDevice方法 ~~~ /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { // device may have just gone offline // sleep a small amount to give ddms state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // ensure bootloader state is updated,最新获取过fastboot devices信息 monitor.waitForDeviceBootloaderStateUpdate(); if (monitor.getDeviceState().equals(TestDeviceState.FASTBOOT)) { Log.i(LOG_TAG, String.format( "Found device %s in fastboot but expected online. Rebooting...", monitor.getSerialNumber())); // TODO: retry if failed getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(), "reboot"); } // wait for device online IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } // occasionally device is erroneously reported as online - double check that we can shell // into device if (!monitor.waitForDeviceShell(mShellWaitTime)) { // treat this as a not available device handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } if (!recoverUntilOnline) { if (monitor.waitForDeviceAvailable(mWaitTime) == null) { // device is online but not responsive handleDeviceUnresponsive(device, monitor); } } } ~~~ 1.首先等待5秒钟,是为了给adb足够的时间先自己处理一下设备断线问题。(adb有自动恢复功能)。 2.我们要等待最新一次的fastboot监听器的执行结果,这个在上一篇文章讲过fastboot监听器是做什么用的。 3.判断目前设备断线的原因是否是因为进入到了fastboot模式下。如果是的话,执行fastboot reboot操作重启设备。 4.等待设备恢复到在线状态(具体怎么等待的,我会在DeviceMonitor中讲解)。 5.如果等待过后,设备没有恢复到在线状态,则调用handleDeviceNotAvailable方法,直接抛出错误。 6.如果设备正常恢复到online状态。再次通过adb命令来检测其正常性,双重验证。如果验证不通过也需要抛出错误。 7.如果第二次验证通过,是否需要等待设备处于有效状态,有效状态和在线状态的区别在于:在线不等于就可以用于测试。可以用于测试的测试需要经过很多中检测。这个也会在以后讲解。 8.如果做了等待设备直到处于有效状态的操作后,设备没有反馈正确的结果,说明设备没有处于有效状态,这个时候我们就需要调用handleDeviceUnresponsive() ~~~ protected void handleDeviceUnresponsive(IDevice device, IDeviceStateMonitor monitor) throws DeviceNotAvailableException { if (!mDisableUnresponsiveReboot) { rebootDevice(device); } IDevice newdevice = monitor.waitForDeviceOnline(); if (newdevice == null) { handleDeviceNotAvailable(monitor, false); return; } if (monitor.waitForDeviceAvailable(mWaitTime) == null) { throw new DeviceUnresponsiveException(String.format( "Device %s is online but unresponsive", monitor.getSerialNumber())); } } ~~~ 9.在上面的方法中,会先判断是否可以重启,如果可以的话,先重启一下设备。 10.然后重复一遍4~7的步骤 11.如果还没成功,就直接报错了。 上面的步骤中在执行设备恢复到设备处于在线状态的的过程中,执行了2次,称为retry的动作。再试一次,可以使成功的概率大一点。 ### recoverDeviceBootloader方法 ~~~ @Override public void recoverDeviceBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { // device may have just gone offline // wait a small amount to give device state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // poll and wait for device to return to valid state long pollTime = mBootloaderWaitTime / BOOTLOADER_POLL_ATTEMPTS; for (int i=0; i < BOOTLOADER_POLL_ATTEMPTS; i++) { if (monitor.waitForDeviceBootloader(pollTime)) { handleDeviceBootloaderUnresponsive(monitor); // passed above check, abort return; } else if (monitor.getDeviceState() == TestDeviceState.ONLINE) { handleDeviceOnlineExpectedBootloader(monitor); return; } } handleDeviceBootloaderNotAvailable(monitor); } ~~~ 1.第一步和上面的意义,等待一段时间,让adb先自己处理。 2.计算重复执行的次数,用总时间/执行的次数,等待每次执行所用的时间。 3.执行等待设备处于BootLoader模式,也就是fastboot模式。 4.如果成功进入fastboot模式,则直接返回,如果设备处于online模式,则调用handleDeviceOnlineExpectedBootloader方法 ~~~ protected void handleDeviceOnlineExpectedBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { Log.i(LOG_TAG, String.format("Found device %s online but expected fastboot.", monitor.getSerialNumber())); // call waitForDeviceOnline to get handle to IDevice IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceBootloaderNotAvailable(monitor); return; } rebootDeviceIntoBootloader(device); if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) { throw new DeviceNotAvailableException(String.format( "Device %s not in bootloader after reboot", monitor.getSerialNumber())); } } ~~~ 5.进入该方法后,首先通过直接monitor.waitForDeviceOnline()来得到该设备,因为该设备就已经处于online,这里调用只是为了得到IDevice. 6.如果这个时候得到的设备为null,直接抛出错误。 7.然后重启设备,进入fastboot模式。 8.然后再次等待设备处于fastboot模式,如果成功,方法结束,回到recoverDeviceBootloader中,然后recoverDeviceBootloader也结束,返回。如果失败,直接抛出异常。 9.重复3~8的过程,如果循环结束后,还无法进入fastboot模式,直接抛出异常。 ### recoverDeviceRecovery方法 因为不可能执行卡刷,所以该方法直接抛出异常。 ~~~ public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } ~~~
';

(17)-fastboot状态监听器

最后更新于:2022-04-01 06:54:58

# Fastboot状态监听器 ## 类图 涉及1个监听器接口,2个实现类,1个执行线程。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddc404eb.jpg) ## 解释 这些类实际运用的是观察者模式,所有的Listener都会被添加到Set中,然后执行线程FastbootMonitor每隔5秒钟运行一次,来检索目前处于fastboot状态下的设备,然后更新设备状态后,通知所有的listener,逐个调用listener的stateUpdated方法通知到各个listener。 ## 代码 监听器接口IFastbootListener: ~~~ public static interface IFastbootListener { /** * Callback when fastboot state has been updated for all devices. */ public void stateUpdated(); } ~~~ 2个实现类,都是DeviceStateMonitor类的私有类。 NotifyFastbootListener:通过Object类的notify通知在该对象上拥有锁的且处于wait状态的对象,你可以往下执行了。 ~~~ private static class NotifyFastbootListener implements IFastbootListener { @Override public void stateUpdated() { synchronized (this) { notify(); } } } ~~~ StubFastbootListener:不做任何事 ~~~ private static class StubFastbootListener implements IFastbootListener { @Override public void stateUpdated() { // ignore } } ~~~ 线程类FastbootMonitor,DeviceManager的私有类。 在该线程run方法中: 1.首先会通过cmd命令“fastboot devices”的执行结果,解析出处于fastboot的设备的SN号。 2.然后判断可分配的设备中是否含有这些设备,如果有,需要更新这些设备的状态至FASTBOOT。 3.接着要判断已分配设备中是否含有这些设备,如果有,需要更新这些设备的状态值NOT_AVAILABLE。(至于为什么和第2项中更新的不一样,以后会讲)。 4.通知所有监听器,我已更新完当次的fastboot状态。 ~~~ private class FastbootMonitor extends Thread { private boolean mQuit = false; FastbootMonitor() { super("FastbootMonitor"); } public void terminate() { mQuit = true; interrupt(); } @Override public void run() { while (!mQuit) { // only poll fastboot devices if there are listeners, as polling // it // indiscriminately can cause fastboot commands to hang if (!mFastbootListeners.isEmpty()) { Set<String> serials = getDevicesOnFastboot(); if (serials != null) { for (String serial : serials) { IManagedTestDevice testDevice = mAllocatedDeviceMap.get(serial); if (testDevice != null && !testDevice.getDeviceState().equals(TestDeviceState.FASTBOOT)) { testDevice.setDeviceState(TestDeviceState.FASTBOOT); } } // now update devices that are no longer on fastboot synchronized (mAllocatedDeviceMap) { for (IManagedTestDevice testDevice : mAllocatedDeviceMap.values()) { if (!serials.contains(testDevice.getSerialNumber()) && testDevice.getDeviceState().equals(TestDeviceState.FASTBOOT)) { testDevice.setDeviceState(TestDeviceState.NOT_AVAILABLE); } } } // create a copy of listeners for notification to // prevent deadlocks Collection<IFastbootListener> listenersCopy = new ArrayList<IFastbootListener>(mFastbootListeners.size()); listenersCopy.addAll(mFastbootListeners); for (IFastbootListener listener : listenersCopy) { listener.stateUpdated(); } } } getRunUtil().sleep(FASTBOOT_POLL_WAIT_TIME); } } } private Set<String> getDevicesOnFastboot() { CommandResult fastbootResult = getRunUtil().runTimedCmd(FASTBOOT_CMD_TIMEOUT, "fastboot", "devices"); if (fastbootResult.getStatus().equals(CommandStatus.SUCCESS)) { CLog.v("fastboot devices returned\n %s", fastbootResult.getStdout()); return parseDevicesOnFastboot(fastbootResult.getStdout()); } else { CLog.w("'fastboot devices' failed. Result: %s, stderr: %s", fastbootResult.getStatus(), fastbootResult.getStderr()); } return null; } static Set<String> parseDevicesOnFastboot(String fastbootOutput) { Set<String> serials = new HashSet<String>(); Pattern fastbootPattern = Pattern.compile("([\\w\\d]+)\\s+fastboot\\s*"); Matcher fastbootMatcher = fastbootPattern.matcher(fastbootOutput); while (fastbootMatcher.find()) { serials.add(fastbootMatcher.group(1)); } return serials; } } ~~~ ## 观察者 哪些地方用到了这个状态监听器呢。也就是那些地方注册成为了观察者。很简单,在DeviceManager类中找到addFastbootListener方法: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddc54166.jpg) 右键弹出对话框,点击 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddc8971e.jpg) 就会找到有哪些地方用到了这个监听器。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddcec428.jpg) 都在DeviceStateMonitor,其实想想也对,上面监听器的2个实现类都是DeviceStateMonitor的私有类,别人也不能用。 ~~~ /** * {@inheritDoc} */ @Override public boolean waitForDeviceBootloader(long time) { if (!mFastbootEnabled) { return false; } long startTime = System.currentTimeMillis(); // ensure fastboot state is updated at least once waitForDeviceBootloaderStateUpdate(); long elapsedTime = System.currentTimeMillis() - startTime; IFastbootListener listener = new StubFastbootListener(); mMgr.addFastbootListener(listener); long waitTime = time - elapsedTime; if (waitTime < 0) { // wait at least 200ms waitTime = 200; } boolean result = waitForDeviceState(TestDeviceState.FASTBOOT, waitTime); mMgr.removeFastbootListener(listener); return result; } ~~~ ~~~ @Override public void waitForDeviceBootloaderStateUpdate() { if (!mFastbootEnabled) { return; } IFastbootListener listener = new NotifyFastbootListener(); synchronized (listener) { mMgr.addFastbootListener(listener); try { listener.wait(); } catch (InterruptedException e) { Log.w(LOG_TAG, "wait for device bootloader state update interrupted"); } } mMgr.removeFastbootListener(listener); } ~~~ ~~~ @Override public boolean waitForDeviceNotAvailable(long waitTime) { IFastbootListener listener = new StubFastbootListener(); if (mFastbootEnabled) { mMgr.addFastbootListener(listener); } boolean result = waitForDeviceState(TestDeviceState.NOT_AVAILABLE, waitTime); if (mFastbootEnabled) { mMgr.removeFastbootListener(listener); } return result; } ~~~
';

(16)-logcat信息收集系统

最后更新于:2022-04-01 06:54:56

# Log收集系统 涉及三个类,关系如图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddc1c629.jpg) ## LogcatReceiver log收集器的外观类,包装了后台执行线程和log内容接收器 ~~~ /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.tradefed.result.InputStreamSource; /** * Class that collects logcat in background. Continues to capture logcat even if device goes * offline then online. */ public class LogcatReceiver { private BackgroundDeviceAction mDeviceAction; private LargeOutputReceiver mReceiver; static final String LOGCAT_CMD = "logcat -v threadtime"; private static final String LOGCAT_DESC = "logcat"; public LogcatReceiver(ITestDevice device, long maxFileSize, int logStartDelay) { mReceiver = new LargeOutputReceiver(LOGCAT_DESC, device.getSerialNumber(), maxFileSize); // FIXME: remove mLogStartDelay. Currently delay starting logcat, as starting // immediately after a device comes online has caused adb instability mDeviceAction = new BackgroundDeviceAction(LOGCAT_CMD, LOGCAT_DESC, device, mReceiver, logStartDelay); } public void start() { mDeviceAction.start(); } public void stop() { mDeviceAction.cancel(); mReceiver.cancel(); mReceiver.delete(); } public InputStreamSource getLogcatData() { return mReceiver.getData(); } public InputStreamSource getLogcatData(int maxBytes) { return mReceiver.getData(maxBytes); } public void clear() { mReceiver.clear(); } } ~~~ 构造方法中初始化后台执行线程BackgoundDeviceAction和log信息接收器LargeOutputReceiver对象。 start:启动线程 stop:关闭现场,取消接收logcat信息,删除收集器中的信息 getLogcatData:得到logcat信息。 getLogcatData(int maxBytes):得到规定大小的logcat信息 clear:清空消息。 ## BackgroundDeviceAction 后台线程,就是在后台线程中执行logcat命令 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache * License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import java.io.IOException; /** * Runs a command on a given device repeating as necessary until the action is canceled. * <p> * When the class is run, the command is run on the device in a separate thread and the output is * collected in a temporary host file. * </p><p> * This is done so: * </p><ul> * <li>if device goes permanently offline during a test, the log data is retained.</li> * <li>to capture more data than may fit in device's circular log.</li> * </ul> */ public class BackgroundDeviceAction extends Thread { private IShellOutputReceiver mReceiver; private ITestDevice mTestDevice; private String mCommand; private String mSerialNumber; private String mDescriptor; private boolean mIsCancelled; private int mLogStartDelay; /** * Creates a {@link BackgroundDeviceAction} * * @param command the command to run * @param descriptor the description of the command. For logging only. * @param device the device to run the command on * @param receiver the receiver for collecting the output of the command * @param startDelay the delay to wait after the device becomes online */ public BackgroundDeviceAction(String command, String descriptor, ITestDevice device, IShellOutputReceiver receiver, int startDelay) { mCommand = command; mDescriptor = descriptor; mSerialNumber = device.getSerialNumber(); mTestDevice = device; mReceiver = receiver; mLogStartDelay = startDelay; // don't keep VM open if this thread is still running setDaemon(true); } /** * {@inheritDoc} * <p> * Repeats the command until canceled. * </p> */ @Override public void run() { while (!isCancelled()) { if (mLogStartDelay > 0) { CLog.d("Sleep for %d before starting %s for %s.", mLogStartDelay, mDescriptor, mSerialNumber); getRunUtil().sleep(mLogStartDelay); } CLog.d("Starting %s for %s.", mDescriptor, mSerialNumber); try { mTestDevice.getIDevice().executeShellCommand(mCommand, mReceiver, 0); } catch (TimeoutException e) { recoverDevice(e.getClass().getName()); } catch (AdbCommandRejectedException e) { recoverDevice(e.getClass().getName()); } catch (ShellCommandUnresponsiveException e) { recoverDevice(e.getClass().getName()); } catch (IOException e) { recoverDevice(e.getClass().getName()); } } } private void recoverDevice(String exceptionType) { CLog.d("%s while running %s on %s. May see duplicated content in log.", exceptionType, mDescriptor, mSerialNumber); // FIXME: Determine when we should append a message to the receiver. if (mReceiver instanceof LargeOutputReceiver) { byte[] stringData = String.format( "%s interrupted. May see duplicated content in log.", mDescriptor).getBytes(); mReceiver.addOutput(stringData, 0, stringData.length); } // Make sure we haven't been cancelled before we sleep for a long time if (isCancelled()) { return; } // sleep a small amount for device to settle getRunUtil().sleep(5 * 1000); // wait a long time for device to be online try { mTestDevice.waitForDeviceOnline(10 * 60 * 1000); } catch (DeviceNotAvailableException e) { CLog.w("Device %s not online", mSerialNumber); } } /** * Cancels the command. */ public synchronized void cancel() { mIsCancelled = true; interrupt(); } /** * If the command is cancelled. */ public synchronized boolean isCancelled() { return mIsCancelled; } /** * Get the {@link RunUtil} instance to use. * <p/> * Exposed for unit testing. */ IRunUtil getRunUtil() { return RunUtil.getDefault(); } } ~~~ ## LargeOutputReceiver 继承adb里面的IShellOutputReceiver接口。用于接收命令执行后返回的信息 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache * License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.ddmlib.IShellOutputReceiver; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.ByteArrayInputStreamSource; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.result.SnapshotInputStreamSource; import com.android.tradefed.util.FixedByteArrayOutputStream; import com.android.tradefed.util.SizeLimitedOutputStream; import com.android.tradefed.util.StreamUtil; import java.io.IOException; import java.io.InputStream; /** * A class designed to help run long running commands collect output. * <p> * The maximum size of the tmp file is limited to approximately {@code maxFileSize}. * To prevent data loss when the limit has been reached, this file keeps set of tmp host * files. * </p> */ public class LargeOutputReceiver implements IShellOutputReceiver { private String mSerialNumber; private String mDescriptor; private boolean mIsCancelled = false; private SizeLimitedOutputStream mOutStream; private long mMaxDataSize; /** * Creates a {@link LargeOutputReceiver}. * * @param descriptor the descriptor of the command to run. For logging only. * @param serialNumber the serial number of the device. For logging only. * @param maxDataSize the approximate max amount of data to keep. */ public LargeOutputReceiver(String descriptor, String serialNumber, long maxDataSize) { mDescriptor = descriptor; mSerialNumber = serialNumber; mMaxDataSize = maxDataSize; mOutStream = createOutputStream(); } /** * {@inheritDoc} */ @Override public synchronized void addOutput(byte[] data, int offset, int length) { if (mIsCancelled || mOutStream == null) { return; } try { mOutStream.write(data, offset, length); } catch (IOException e) { CLog.w("failed to write %s data for %s.", mDescriptor, mSerialNumber); } } /** * Gets the collected output as a {@link InputStreamSource}. * * @return The collected output from the command. */ public synchronized InputStreamSource getData() { if (mOutStream != null) { try { return new SnapshotInputStreamSource(mOutStream.getData()); } catch (IOException e) { CLog.e("failed to get %s data for %s.", mDescriptor, mSerialNumber); CLog.e(e); } } // return an empty InputStreamSource return new ByteArrayInputStreamSource(new byte[0]); } /** * Gets the last <var>maxBytes</var> of collected output as a {@link InputStreamSource}. * * @param maxBytes the maximum amount of data to return. Should be an amount that can * comfortably fit in memory * @return The collected output from the command, stored in memory */ public synchronized InputStreamSource getData(final int maxBytes) { if (mOutStream != null) { InputStream fullStream = null; try { fullStream = mOutStream.getData(); final FixedByteArrayOutputStream os = new FixedByteArrayOutputStream(maxBytes); StreamUtil.copyStreams(fullStream, os); return new InputStreamSource() { @Override public InputStream createInputStream() { return os.getData(); } @Override public void cancel() { // ignore, nothing to do } @Override public long size() { return os.size(); } }; } catch (IOException e) { CLog.e("failed to get %s data for %s.", mDescriptor, mSerialNumber); CLog.e(e); } finally { StreamUtil.close(fullStream); } } // return an empty InputStreamSource return new ByteArrayInputStreamSource(new byte[0]); } /** * {@inheritDoc} */ @Override public synchronized void flush() { if (mOutStream == null) { return; } mOutStream.flush(); } /** * Delete currently accumulated data, and then re-create a new file. */ public synchronized void clear() { delete(); mOutStream = createOutputStream(); } private SizeLimitedOutputStream createOutputStream() { return new SizeLimitedOutputStream(mMaxDataSize, String.format("%s_%s", getDescriptor(), mSerialNumber), ".txt"); } /** * Cancels the command. */ public synchronized void cancel() { mIsCancelled = true; } /** * Delete all accumulated data. */ public void delete() { mOutStream.delete(); mOutStream = null; } /** * {@inheritDoc} */ @Override public synchronized boolean isCancelled() { return mIsCancelled; } /** * Get the descriptor. * <p> * Exposed for unit testing. * </p> */ String getDescriptor() { return mDescriptor; } } ~~~
';

(15)-任务执行完

最后更新于:2022-04-01 06:54:53

case执行完毕后,会回到CtsTest的run方法中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd952022.jpg) 这个时候会先将mRemainingTestPkgs列表的第一项移除,以便下一次取第一个的时候,取的是新的TestPackage对象,然后根据case的类别来做相应的重启操作,最后返回到主界面。最后,截图留下犯罪现场,下载之前安装的jar包。在finally语句块中report未执行的case。为执行的case是通过执行其testStarted方法,但是不执行testEnded方法,让监听器识别出这条case未执行。 当CtsTest的run方法执行完毕后,回到了TestInvocation.prepareAndRun中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd9cc783.jpg) 执行完恢复操作以后,会返回到performInvocation中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dda48309.jpg) 将日志系统打印出来并保存到logs目录文件中。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dda82e09.jpg) 然后调用InvocationSummaryHelper.reportInvocationEnded方法: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddab7be5.jpg) 通知所有监听器,本次任务的彻底结束,各自该干嘛,干嘛去。然后回到invoke中,然后invoke也完成了,回到了InvocationThread.run方法中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddb22c0d.jpg) 然后释放设备。移除执行线程。最后返回到CommandScheduler.run方法 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911ddba886b.jpg) 我们的讲解也结束了,我们也该干嘛干嘛去吧。散了吧!!
';

(14)-任务执行过程

最后更新于:2022-04-01 06:54:51

上一篇文章我们已经知道testcases目录中xml配置文件读取出来后的形式,继续往下看: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd452703.jpg) 然后把xml对应的TestPackageDef保存到Map中,所以我们可以这样说,TestPackageDef就代表了一个testcases目录下的xml文件。所以有多少个xml文件就有多少个TestPackageDef对象,然后将这些对象保存到map中。key值为xml的appPackageName属性值。当所有的xml文件都配置完成后,我们就回到了buildTestsToRun方法中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd5df798.jpg) 这个时候调用了getTestPackagesToRun,这个方法是从刚才得到的testRepo对象中筛选出本次任务需要跑的ITestPackageDef列表。再根据该列表组装List 对象testPkgList并返回。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd6580ab.jpg) 然乎根据参数来添加发生错误时的操作: 保存bugreport信息 保存截图 保存logcat system的信息 然后找到要安装的apk和执行完任务后要卸载的包名: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd6a7631.jpg) 这是根据xml中targetBinaryName属性对应的值找到apk名和targetNameSpace属性找到要卸载的包名。然后安装这些apk,并获取手机设备信息。实际上是通过安装TestDeviceSetup.apk,然后运行Instrumentation的case来获取信息的。获取完信息以后,会判断是否需要重启,只有当要执行的case包大于1且mDisableReboot为false时才重启设备。然后是循环执行测试任务,以包为单位执行,执行的核心为test.run(filter); ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd738a4e.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd7a4c85.jpg) 先将包含测试的apk安装到设备中,然后运行case,最后删除case包。我们来看看删除的case包到底是什么。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd7d118d.jpg) 原来apk安装后,android系统是通过其包名来定位apk的,所以卸载的时候肯定要用包名。先来再来回头看super.run(listener); ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd80a0c5.jpg) 先通过DDM的RemoteAndroidTestRunner来创建测试的runner,然后设置一些基本参数,就可以调用doTestRun方法来进行实际的测试。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd88abb2.jpg) 先获得要测试的case信息。经过一系列跳来跳去跳来跳去,最后到了TestDevice中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd8e4177.jpg) 最后到执行case,执行完以后会判断是否成功发送了命令,如果没有就要重连设备。最后讲一下performDeviceAction这个方法: ~~~ private boolean performDeviceAction(String actionDescription, final DeviceAction action, int retryAttempts) throws DeviceNotAvailableException { // 如果成功直接返回,如果失败就要重试 for (int i = 0; i < retryAttempts + 1; i++) { try { return action.run(); } catch (TimeoutException e) { logDeviceActionException(actionDescription, e); } catch (IOException e) { logDeviceActionException(actionDescription, e); } catch (InstallException e) { logDeviceActionException(actionDescription, e); } catch (SyncException e) { logDeviceActionException(actionDescription, e); // a SyncException is not necessarily a device communication // problem // do additional diagnosis if (!e.getErrorCode().equals(SyncError.BUFFER_OVERRUN) && !e.getErrorCode().equals(SyncError.TRANSFER_PROTOCOL_ERROR)) { // this is a logic problem, doesn't need recovery or to be // retried return false; } } catch (AdbCommandRejectedException e) { logDeviceActionException(actionDescription, e); } catch (ShellCommandUnresponsiveException e) { CLog.w("Device %s stopped responding when attempting %s", getSerialNumber(), actionDescription); } // TODO: currently treat all exceptions the same. In future consider // different recovery // mechanisms for time out's vs IOExceptions recoverDevice(); } if (retryAttempts > 0) { throw new DeviceUnresponsiveException(String.format("Attempted %s multiple times " + "on device %s without communication success. Aborting.", actionDescription, getSerialNumber())); } return false; ~~~ 上面的写法会在case执行过程中出现异常的话,会有重试机制。但前提是retryAttempts这个变量的值要大于0。
';

(13)-任务执行过程

最后更新于:2022-04-01 06:54:49

因为测试任务是个很复杂的过程,所以要单独拿出来讲,里面还涉及了result_reporter的内容。所以这是一个大块。首先把断点打在CtsTest的run方法中,删除其他断点,重新启动debug模式: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcfbf684.jpg) 首先会调用checkFields检查一下命令行参数。然后生成plan里的包名信息。(要理解plan的意思,plan就是cts目录下plan文件下的xml文件,它里面配置的entry代表一个测试项,一个测试项里又包含多个测试的case)。本程序执行的是Signature计划,我们就来看看这个xml文件中的内容。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd03391d.jpg) ~~~ <?xml version="1.0" encoding="UTF-8"?> <TestPlan version="1.0"> <Entry uri="android.tests.sigtest"/> </TestPlan> ~~~ 里面就包含一个测试项。再通过这个去testcases目录去找这个测试项的配置文件: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd06b342.jpg) 思考一个问题,我们是如何根据Entry的配置找到testcases下的SignatureTest.xml文件的。要理解通过 ~~~ <Entry uri="android.tests.sigtest"/> ~~~ 找测试项,并不是说这个测试项的xml名为android.tests.sigtest,而是你要打开xml文件去里面找一个叫appNameSpace属性名,这个名称才是Entry配置时参照的内容,这个时候我们打开SignatureTest.xml,来验证一下是否相同: ~~~ <?xml version="1.0" encoding="UTF-8"?> <TestPackage AndroidFramework="Android 1.0" appNameSpace="android.tests.sigtest" appPackageName="android.tests.sigtest" jarPath="" name="SignatureTest" runner=".InstrumentationRunner" signatureCheck="true" targetBinaryName="" targetNameSpace="" version="1.0"> <TestSuite name="android"> <TestSuite name="tests"> <TestSuite name="sigtest"> <TestCase name="SignatureTest"> <Test name="testSignature"/> </TestCase> </TestSuite> </TestSuite> </TestSuite> </TestPackage> ~~~ 很巧,刚好一样,一会我们会在程序的debug过程中,验证这一点。再回到程序中。 检查完参数后,就要根据plan获取测试项: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd0a8889.jpg) 进入buildTestToRun方法中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd111d14.jpg) 该方法中会先调用createTestCaseRepo得到testcases所有的测试对象(一个xml文件代表一个测试对象)。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd1642ff.jpg) 如上,new一个TestPackageRepo对象,参数为File对象(指向testcases目录)和boolean值。进入TestPackageRepo对象的构造方法,看其如何获得xml文件的信息的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd19a2c5.jpg) 直接看parse()方法 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd1c6316.jpg) parse方法中,首先获得testcases目录中所有的xml文件。查看Variables一栏中xmlFiles对象。有80个元素,说明testcases目录下有81个xml文件。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd1eda17.jpg) 然后parse方法会逐个遍历这些xml文件。转到parseTestFromXml(xmlFile)方法中: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd22280b.jpg) 先创建一个TestPackageXmlParser对象,该对象继承于AbstractXmlParser,通过SAX的方式解析xml文件。有关SAX如何解析xml文件的内容我就不多阐述了。parser.parse(createStreamFromFile(xmlFile));按F5,调用的是TestPackageHandler类中的startElement方法。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd292b64.jpg) 该方法执行完成后,xml文件里的信息都读到了TestPackageDef对象中了。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd32f8a0.jpg) 此时我们来看经过TestPackageXmlParser.parse方法解析的xml文件是以什么形式保存的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd3efe5c.jpg) 对应的属性都存在了TestPackageDef对应的属性中,要执行的case保存在mTests的列表中。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dd41fcae.jpg)
';

(12)-ITargetPreparer

最后更新于:2022-04-01 06:54:47

测试开启前的设备系统准备工作。 # 接口 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.targetprep; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; /** * Prepares the test environment for the test run. * <p/> * For example, installs software, tweaks env settings for testing, launches targets etc. * <p/> * Note that multiple {@link ITargetPreparer} can specified in a configuration. It is recommended * that each ITargetPreparer clearly document its expected environment pre-setup and post-setUp. * e.g. a ITargetPreparer that configures a device for testing must be run after the ITargetPreparer * that installs software. */ public interface ITargetPreparer { /** * Perform the target setup for testing. * * @param device the {@link ITestDevice} to prepare. * @param buildInfo data about the build under test. * @throws TargetSetupError if fatal error occurred setting up environment * @throws DeviceNotAvailableException if device became unresponsive */ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, BuildError, DeviceNotAvailableException; } ~~~ 就一个方法:setUp(),比如你要安装系统、安装apk或者其他都是case要求的安装事务都要在这个方法中完成。 # 实现类 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.targetprep; import com.android.ddmlib.Log; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.device.ITestDevice; /** * Placeholder empty implementation of a {@link ITargetPreparer}. */ public class StubTargetPreparer implements ITargetPreparer { /** * {@inheritDoc} */ @Override public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError { Log.d("TargetPreparer", "skipping target prepare step"); } } ~~~ 这个类里面的方法就是打印了一句话,没做任何处理。但是真正要是满足自己特定的需求就要自己写一个类继承与该接口才行。
';

(11)-ICommandOptions

最后更新于:2022-04-01 06:54:44

命令行选项就是你在敲run cts --plan UI命令时可以再跟一个参数,比如在debug配置参数时加入--help看一下效果。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcf52061.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcf742e3.jpg) 所以这里面定义的类一般是可以在命令行上加参数的形更改的。先来看一下里面有哪些参数 # 接口 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.command; /** * Container for execution options for commands. */ public interface ICommandOptions { public boolean isNeedPrepare(); public void setNeedPrepare(boolean needPrepare); public boolean isNeedTearDown(); public void setNeedTearDown(boolean needTearDown); /** * Returns <code>true</code> if abbreviated help mode has been requested */ public boolean isHelpMode(); /** * Returns <code>true</code> if full detailed help mode has been requested */ public boolean isFullHelpMode(); /** * Return <code>true</code> if we should <emph>skip</emph> adding this command to the queue. */ public boolean isDryRunMode(); /** * Return <code>true</code> if we should print the command out to the console before we * <emph>skip</emph> adding it to the queue. */ public boolean isNoisyDryRunMode(); /** * Return the loop mode for the config. */ public boolean isLoopMode(); /** * Get the min loop time for the config. */ public long getMinLoopTime(); /** * Sets the loop mode for the command * * @param loopMode */ public void setLoopMode(boolean loopMode); /** * Creates a copy of the {@link ICommandOptions} object. * @return */ public ICommandOptions clone(); /** * Return true if command should run on all devices. */ public boolean runOnAllDevices(); } ~~~ # 实现类 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.command; import com.android.tradefed.config.ConfigurationException; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.config.OptionCopier; import com.android.tradefed.log.LogUtil.CLog; /** * Implementation of {@link ICommandOptions}. */ public class CommandOptions implements ICommandOptions { @Option(name = "help", description = "display the help text for the most important/critical options.", importance = Importance.ALWAYS) private boolean mHelpMode = false; @Option(name = "help-all", description = "display the full help text for all options.", importance = Importance.ALWAYS) private boolean mFullHelpMode = false; @Option(name = "dry-run", description = "build but don't actually run the command. Intended as a quick check " + "to ensure that a command is runnable.", importance = Importance.ALWAYS) private boolean mDryRunMode = false; @Option(name = "noisy-dry-run", description = "build but don't actually run the command. This version prints the " + "command to the console. Intended for cmdfile debugging.", importance = Importance.ALWAYS) private boolean mNoisyDryRunMode = false; @Option(name = "min-loop-time", description = "the minimum invocation time in ms when in loop mode.") private long mMinLoopTime = 10 * 60 * 1000; @Option(name = "loop", description = "keep running continuously.") private boolean mLoopMode = true; @Option(name = "all-devices", description = "fork this command to run on all connected devices.") private boolean mAllDevices = true; @Option(name = "need-prepare", description = "is needed to prepare device") private boolean mNeedPrepare = true; // @Option(name = "need-flash", description = "is needed to fastboot device") // private boolean mNeedFlash = true; @Option(name = "need-tearDown", description = "is needed to clean device") private boolean mNeedTearDown = true; public boolean isNeedPrepare() { return mNeedPrepare; } public void setNeedPrepare(boolean needPrepare) { mNeedPrepare = needPrepare; } public boolean isNeedTearDown() { return mNeedTearDown; } public void setNeedTearDown(boolean needTearDown) { mNeedTearDown = needTearDown; } /** * Set the help mode for the config. * <p/> * Exposed for testing. */ void setHelpMode(boolean helpMode) { mHelpMode = helpMode; } /** * {@inheritDoc} */ @Override public boolean isHelpMode() { return mHelpMode; } /** * {@inheritDoc} */ @Override public boolean isFullHelpMode() { return mFullHelpMode; } /** * Set the dry run mode for the config. * <p/> * Exposed for testing. */ void setDryRunMode(boolean dryRunMode) { mDryRunMode = dryRunMode; } /** * {@inheritDoc} */ @Override public boolean isDryRunMode() { return mDryRunMode || mNoisyDryRunMode; } /** * {@inheritDoc} */ @Override public boolean isNoisyDryRunMode() { return mNoisyDryRunMode; } /** * Set the loop mode for the config. */ @Override public void setLoopMode(boolean loopMode) { mLoopMode = loopMode; } /** * {@inheritDoc} */ @Override public boolean isLoopMode() { return mLoopMode; } /** * Set the min loop time for the config. * <p/> * Exposed for testing. */ void setMinLoopTime(long loopTime) { mMinLoopTime = loopTime; } /** * {@inheritDoc} */ @Override public long getMinLoopTime() { return mMinLoopTime; } @Override public ICommandOptions clone() { CommandOptions clone = new CommandOptions(); try { OptionCopier.copyOptions(this, clone); } catch (ConfigurationException e) { CLog.e("failed to clone command options", e); } return clone; } /** * {@inheritDoc} */ @Override public boolean runOnAllDevices() { return mAllDevices; } } ~~~ 实现类里定义了接口中的方法对应的属性分别是: help就是你要在命令行后的参数,就如文章一开头我添加的--help模式。如果你添加了就等于mHelpMode = true;打印帮助信息,但是只有注解中importance属性的才打印 ~~~ @Option(name = "help", description = "display the help text for the most important/critical options.", importance = Importance.ALWAYS) private boolean mHelpMode = false; ~~~ 和上面一样,但是会打印所有option注解的信息,不管有没有importance选项 ~~~ @Option(name = "help-all", description = "display the full help text for all options.", importance = Importance.ALWAYS) private boolean mFullHelpMode = false; ~~~ 测试命令是否可行。 ~~~ @Option(name = "dry-run", description = "build but don't actually run the command. Intended as a quick check " + "to ensure that a command is runnable.", importance = Importance.ALWAYS) private boolean mDryRunMode = false; @Option(name = "noisy-dry-run", description = "build but don't actually run the command. This version prints the " + "command to the console. Intended for cmdfile debugging.", importance = Importance.ALWAYS) private boolean mNoisyDryRunMode = false; ~~~ 是否循环执行命令以及循环的时间 ~~~ @Option(name = "min-loop-time", description = "the minimum invocation time in ms when in loop mode.") private long mMinLoopTime = 10 * 60 * 1000; @Option(name = "loop", description = "keep running continuously.") private boolean mLoopMode = true; ~~~ 是否全设备测试 ~~~ @Option(name = "all-devices", description = "fork this command to run on all connected devices.") private boolean mAllDevices = true; ~~~ 测试前的准备工作和测试后的还原工作 ~~~ @Option(name = "need-prepare", description = "is needed to prepare device") private boolean mNeedPrepare = true; @Option(name = "need-tearDown", description = "is needed to clean device") private boolean mNeedTearDown = true; ~~~
';

(10)-TestDeviceOptions

最后更新于:2022-04-01 06:54:42

设备参数的意思是说,在执行过程中,对被测设备需要做的一些操作,需要你cts框架对设备做哪些操作,你才能做,不允许你做的,你想做也做不了。比如需不需要你解锁,需要的话,你就得去解锁。是否允许你cts重启机器等,该类配置对应于device_options。只有类没有接口,一般也不需要自己写特殊的类,用原生的基本就能满足需求。 ~~~ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.tradefed.config.Option; /** * Container for {@link ITestDevice} {@link Option}s */ public class TestDeviceOptions { @Option(name = "enable-root", description = "enable adb root on boot.") private boolean mEnableAdbRoot = true; @Option(name = "<a target=_blank href="http://blog.csdn.net/qinjuning/article/details/7505703">disable-keyguard</a>", description = "attempt to disable keyguard once boot is complete.") private boolean mDisableKeyguard = true; @Option(name = "disable-keyguard-cmd", description = "shell command to disable keyguard.") private String mDisableKeyguardCmd = "input keyevent 82"; @Option(name = "max-tmp-logcat-file", description = "The maximum size of tmp logcat data to retain, in bytes.") private long mMaxLogcatDataSize = 20 * 1024 * 1024; @Option(name = "fastboot-timeout", description = "time in ms to wait for a device to boot into fastboot.") private int mFastbootTimeout = 1 * 60 * 1000; @Option(name = "adb-recovery-timeout", description = "time in ms to wait for a device to boot into recovery.") private int mAdbRecoveryTimeout = 1 * 60 * 1000; @Option(name = "reboot-timeout", description = "time in ms to wait for a device to reboot to full system.") private int mRebootTimeout = 2 * 60 * 1000; @Option(name = "use-fastboot-erase", description = "use fastboot erase instead of fastboot format to wipe partitions") private boolean mUseFastbootErase = false; @Option(name = "unencrypt-reboot-timeout", description = "time in ms to wait for the device to " + "format the filesystem and reboot after unencryption") private int mUnencryptRebootTimeout = 0; @Option(name = "online-timeout", description = "default time in ms to wait for the device to " + "be visible on adb.") private long mOnlineTimeout = 1 * 60 * 1000; @Option(name = "available-timeout", description = "default time in ms to wait for the device " + "to be available aka fully boot.") private long mAvailableTimeout = 6 * 60 * 1000; @Option(name = "device-comm-port", description = "comm port related to this device") private String mCommPort = null; public String getCommPort() { return mCommPort; } public void setCommPort(String commPort) { mCommPort = commPort; } /** * Check whether adb root should be enabled on boot for this device */ public boolean isEnableAdbRoot() { return mEnableAdbRoot; } /** * Set whether adb root should be enabled on boot for this device */ public void setEnableAdbRoot(boolean enableAdbRoot) { mEnableAdbRoot = enableAdbRoot; } /** * Check whether or not we should attempt to disable the keyguard once boot has completed */ public boolean isDisableKeyguard() { return mDisableKeyguard; } /** * Set whether or not we should attempt to disable the keyguard once boot has completed */ public void setDisableKeyguard(boolean disableKeyguard) { mDisableKeyguard = disableKeyguard; } /** * Fetch the command to disable the keyguard */ public String getDisableKeyguardCmd() { return mDisableKeyguardCmd; } /** * Set the command to be used to disable the keyguard */ public void setDisableKeyguardCmd(String disableKeyguardCmd) { mDisableKeyguardCmd = disableKeyguardCmd; } /** * Get the approximate maximum size of a tmp logcat data to retain, in bytes. */ public long getMaxLogcatDataSize() { return mMaxLogcatDataSize; } /** * Set the approximate maximum size of a tmp logcat to retain, in bytes */ public void setMaxLogcatDataSize(long maxLogcatDataSize) { mMaxLogcatDataSize = maxLogcatDataSize; } /** * @return the timeout to boot into fastboot mode in msecs. */ public int getFastbootTimeout() { return mFastbootTimeout; } /** * @param fastbootTimeout the timout in msecs to boot into fastboot mode. */ public void setFastbootTimeout(int fastbootTimeout) { mFastbootTimeout = fastbootTimeout; } /** * @return the timeout in msecs to boot into recovery mode. */ public int getAdbRecoveryTimeout() { return mAdbRecoveryTimeout; } /** * @param adbRecoveryTimeout the timeout in msecs to boot into recovery mode. */ public void setAdbRecoveryTimeout(int adbRecoveryTimeout) { mAdbRecoveryTimeout = adbRecoveryTimeout; } /** * @return the timeout in msecs for the full system boot. */ public int getRebootTimeout() { return mRebootTimeout; } /** * @param rebootTimeout the timeout in msecs for the system to fully boot. */ public void setRebootTimeout(int rebootTimeout) { mRebootTimeout = rebootTimeout; } /** * @return whether to use fastboot erase instead of fastboot format to wipe partitions. */ public boolean getUseFastbootErase() { return mUseFastbootErase; } /** * @param useFastbootErase whether to use fastboot erase instead of fastboot format to wipe * partitions. */ public void setUseFastbootErase(boolean useFastbootErase) { mUseFastbootErase = useFastbootErase; } /** * @return the timeout in msecs for the filesystem to be formatted and the device to reboot * after unencryption. */ public int getUnencryptRebootTimeout() { return mUnencryptRebootTimeout; } /** * @param unencryptRebootTimeout the timeout in msecs for the filesystem to be formatted and * the device to reboot after unencryption. */ public void setUnencryptRebootTimeout(int unencryptRebootTimeout) { mUnencryptRebootTimeout = unencryptRebootTimeout; } /** * @return the default time in ms to to wait for a device to be online. */ public long getOnlineTimeout() { return mOnlineTimeout; } public void setOnlineTimeout(long onlineTimeout) { mOnlineTimeout = onlineTimeout; } /** * @return the default time in ms to to wait for a device to be available. */ public long getAvailableTimeout() { return mAvailableTimeout; } } ~~~ 该类的属性都有说明,很容易理解,无非就是能否root,需不需要解锁,解锁命令比较重要:input keyevent 82,82代表的是menu键,但是不是所有手机都有效的,如果无效,需要考虑一下用什么方法能解锁你的手机。
';

(9)-IDeviceRecovery

最后更新于:2022-04-01 06:54:40

当设备处于offline状态时,cts框架就要调用IDeviceRecovery接口类去做相应的恢复工作。 # 接口 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; /** * Interface for recovering a device that has gone offline. */ public interface IDeviceRecovery { /** * Attempt to recover the given device that can no longer be communicated with. * <p/> * Method should block and only return when device is in requested state. * * @param monitor the {@link IDeviceStateMonitor} to use. * @param recoverUntilOnline if true, method should return as soon as device is online on adb. * If false, method should block until device is fully available for testing (ie * {@link IDeviceStateMonitor#waitForDeviceAvailable()} succeeds. * @throws DeviceNotAvailableException if device could not be recovered */ public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException; /** * Attempt to recover the given unresponsive device in recovery mode. * * @param monitor the {@link IDeviceStateMonitor} to use. * @throws DeviceNotAvailableException if device could not be recovered */ public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException; /** * Attempt to recover the given unresponsive device in bootloader mode. * * @param monitor the {@link IDeviceStateMonitor} to use. * @throws DeviceNotAvailableException if device could not be recovered */ public void recoverDeviceBootloader(IDeviceStateMonitor monitor) throws DeviceNotAvailableException; } ~~~ 该接口中要三个方法: recoverDevice:连接不再通信的设备 recoverDeviceRecovery:当手机处于[Recovery](http://www.anqu.com/lab_74/21078/)工程模式下,已经发送信息,但是没有反应,就需要调用该方法再次重连。 recoverDeviceBootloader:恢复没有反馈的设备,与上面不同的是当前设备处于[BootLoader](http://mobile.zol.com.cn/242/2424698_all.html)模式下。 # 实现类 cts模式的实现类为WaitDeviceRecovery: ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.device; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.IDevice; import com.android.ddmlib.Log; import com.android.ddmlib.TimeoutException; import com.android.tradefed.config.Option; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import java.io.IOException; /** * A simple implementation of a {@link IDeviceRecovery} that waits for device to be online and * respond to simple commands. */ public class WaitDeviceRecovery implements IDeviceRecovery { private static final String LOG_TAG = "WaitDeviceRecovery"; /** the time in ms to wait before beginning recovery attempts */ protected static final long INITIAL_PAUSE_TIME = 5 * 1000; /** * The number of attempts to check if device is in bootloader. * <p/> * Exposed for unit testing */ public static final int BOOTLOADER_POLL_ATTEMPTS = 3; // TODO: add a separate configurable timeout per operation @Option(name="device-wait-time", description="maximum time in ms to wait for a single device recovery command.") protected long mWaitTime = 4 * 60 * 1000; @Option(name="bootloader-wait-time", description="maximum time in ms to wait for device to be in fastboot.") protected long mBootloaderWaitTime = 30 * 1000; @Option(name="shell-wait-time", description="maximum time in ms to wait for device shell to be responsive.") protected long mShellWaitTime = 30 * 1000; @Option(name = "disable-unresponsive-reboot", description = "If this is set, we will not attempt to reboot an unresponsive device" + "that is in userspace. Note that this will have no effect if the device is in " + "fastboot or is expected to be in fastboot.") protected boolean mDisableUnresponsiveReboot = false; /** * Get the {@link RunUtil} instance to use. * <p/> * Exposed for unit testing. */ protected IRunUtil getRunUtil() { return RunUtil.getDefault(); } /** * Sets the maximum time in ms to wait for a single device recovery command. */ void setWaitTime(long waitTime) { mWaitTime = waitTime; } /** * {@inheritDoc} */ @Override public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) throws DeviceNotAvailableException { // device may have just gone offline // sleep a small amount to give ddms state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // ensure bootloader state is updated monitor.waitForDeviceBootloaderStateUpdate(); if (monitor.getDeviceState().equals(TestDeviceState.FASTBOOT)) { Log.i(LOG_TAG, String.format( "Found device %s in fastboot but expected online. Rebooting...", monitor.getSerialNumber())); // TODO: retry if failed getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(), "reboot"); } // wait for device online IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } // occasionally device is erroneously reported as online - double check that we can shell // into device if (!monitor.waitForDeviceShell(mShellWaitTime)) { // treat this as a not available device handleDeviceNotAvailable(monitor, recoverUntilOnline); return; } if (!recoverUntilOnline) { if (monitor.waitForDeviceAvailable(mWaitTime) == null) { // device is online but not responsive handleDeviceUnresponsive(device, monitor); } } } /** * Handle situation where device is online but unresponsive. * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceUnresponsive(IDevice device, IDeviceStateMonitor monitor) throws DeviceNotAvailableException { if (!mDisableUnresponsiveReboot) { rebootDevice(device); } IDevice newdevice = monitor.waitForDeviceOnline(); if (newdevice == null) { handleDeviceNotAvailable(monitor, false); return; } if (monitor.waitForDeviceAvailable(mWaitTime) == null) { throw new DeviceUnresponsiveException(String.format( "Device %s is online but unresponsive", monitor.getSerialNumber())); } } /** * Handle situation where device is not available. * * @param monitor the {@link IDeviceStateMonitor} * @param recoverTillOnline if true this method should return if device is online, and not * check for responsiveness * @throws DeviceNotAvailableException */ protected void handleDeviceNotAvailable(IDeviceStateMonitor monitor, boolean recoverTillOnline) throws DeviceNotAvailableException { throw new DeviceNotAvailableException(String.format("Could not find device %s", monitor.getSerialNumber())); } /** * {@inheritDoc} */ @Override public void recoverDeviceBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { // device may have just gone offline // wait a small amount to give device state a chance to settle // TODO - see if there is better way to handle this Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover", INITIAL_PAUSE_TIME, monitor.getSerialNumber())); getRunUtil().sleep(INITIAL_PAUSE_TIME); // poll and wait for device to return to valid state long pollTime = mBootloaderWaitTime / BOOTLOADER_POLL_ATTEMPTS; for (int i=0; i < BOOTLOADER_POLL_ATTEMPTS; i++) { if (monitor.waitForDeviceBootloader(pollTime)) { handleDeviceBootloaderUnresponsive(monitor); // passed above check, abort return; } else if (monitor.getDeviceState() == TestDeviceState.ONLINE) { handleDeviceOnlineExpectedBootloader(monitor); return; } } handleDeviceBootloaderNotAvailable(monitor); } /** * Handle condition where device is online, but should be in bootloader state. * <p/> * If this method * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceOnlineExpectedBootloader(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { Log.i(LOG_TAG, String.format("Found device %s online but expected fastboot.", monitor.getSerialNumber())); // call waitForDeviceOnline to get handle to IDevice IDevice device = monitor.waitForDeviceOnline(); if (device == null) { handleDeviceBootloaderNotAvailable(monitor); return; } rebootDeviceIntoBootloader(device); if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) { throw new DeviceNotAvailableException(String.format( "Device %s not in bootloader after reboot", monitor.getSerialNumber())); } } /** * @param monitor * @throws DeviceNotAvailableException */ protected void handleDeviceBootloaderUnresponsive(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { CLog.i("Found device %s in fastboot but potentially unresponsive.", monitor.getSerialNumber()); // TODO: retry reboot getRunUtil().runTimedCmd(20*1000, "fastboot", "-s", monitor.getSerialNumber(), "reboot-bootloader"); // wait for device to reboot monitor.waitForDeviceNotAvailable(20*1000); if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) { throw new DeviceNotAvailableException(String.format( "Device %s not in bootloader after reboot", monitor.getSerialNumber())); } } /** * Reboot device into bootloader. * * @param device the {@link IDevice} to reboot. */ protected void rebootDeviceIntoBootloader(IDevice device) { try { device.reboot("bootloader"); } catch (IOException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } catch (TimeoutException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber())); } catch (AdbCommandRejectedException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } } /** * Reboot device into bootloader. * * @param device the {@link IDevice} to reboot. */ protected void rebootDevice(IDevice device) { try { device.reboot(null); } catch (IOException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } catch (TimeoutException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber())); } catch (AdbCommandRejectedException e) { Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(), e.getMessage())); } } /** * Handle situation where device is not available when expected to be in bootloader. * * @param monitor the {@link IDeviceStateMonitor} * @throws DeviceNotAvailableException */ protected void handleDeviceBootloaderNotAvailable(final IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException(String.format( "Could not find device %s in bootloader", monitor.getSerialNumber())); } /** * {@inheritDoc} */ @Override public void recoverDeviceRecovery(IDeviceStateMonitor monitor) throws DeviceNotAvailableException { throw new DeviceNotAvailableException("device recovery not implemented"); } } ~~~ 这个类的方法中具体讲了如何实现重连机制,有兴趣的可以详细了解。我不太感兴趣,就一笔带过了。
';

(8)-IBuildProvider

最后更新于:2022-04-01 06:54:37

IBuildProvider接口中定义了三个方法 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.build; /** * Responsible for providing info regarding the build under test. */ public interface IBuildProvider { /** * Retrieve the data for build under test. * * @return the {@link IBuildInfo} for build under test or <code>null</code> if no build is * available for testing * @throws BuildRetrievalError if build info failed to be retrieved due to an unexpected error */ public IBuildInfo getBuild() throws BuildRetrievalError; /** * Mark the given build as untested. * <p/> * Called in cases where TradeFederation has failed to complete testing on the build due to an * environment problem. * * @param info the {@link IBuildInfo} to reset */ public void buildNotTested(IBuildInfo info); /** * Clean up any temporary build files. */ public void cleanUp(IBuildInfo info); } ~~~ 该类主要是为了提供IBuildInfo信息的。所以目光转到IBuildInfo接口中: ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.build; import com.android.tradefed.device.ITestDevice; import java.io.File; import java.util.Collection; import java.util.Map; /** * Holds information about the build under test. */ public interface IBuildInfo { /** * Default value when build ID is unknown. */ public final static String UNKNOWN_BUILD_ID = "-1"; /** * Returns the unique identifier of build under test. Should never be null. Defaults to * {@link #UNKNOWN_BUILD_ID}. */ public String getBuildId(); /** * Return a unique name for the tests being run. */ public String getTestTag(); /** * Return complete name for the build being tested. * <p/> * A common implementation is to construct the build target name from a combination of * the build flavor and branch name. [ie (branch name)-(build flavor)] */ public String getBuildTargetName(); /** * Optional method to return the type of build being tested. * <p/> * A common implementation for Android platform builds is to return * (build product)-(build os)-(build variant). * ie generic-linux-userdebug * * @return the build flavor or <code>null</code> if unset/not applicable */ public String getBuildFlavor(); /** * @return the {@link ITestDevice} serial that this build was executed on. Returns <code>null * </code> if no device is associated with this build. */ public String getDeviceSerial(); /** * Set the build flavor. * * @param buildFlavor */ public void setBuildFlavor(String buildFlavor); /** * Optional method to return the source control branch that the build being tested was * produced from. * * @return the build branch or <code>null</code> if unset/not applicable */ public String getBuildBranch(); /** * Set the build branch * * @param branch the branch name */ public void setBuildBranch(String branch); /** * Set the {@link ITestDevice} serial associated with this build. * * @param serial the serial number of the {@link ITestDevice} that this build was executed with. */ public void setDeviceSerial(String serial); /** * Get a set of name-value pairs of additional attributes describing the build. * * @return a {@link Map} of build attributes. Will not be <code>null</code>, but may be empty. */ public Map<String, String> getBuildAttributes(); /** * Add a build attribute * * @param attributeName the unique attribute name * @param attributeValue the attribute value */ public void addBuildAttribute(String attributeName, String attributeValue); /** * Helper method to retrieve a file with given name. * @param name * @return the image file or <code>null</code> if not found */ public File getFile(String name); /** * Returns all {@link VersionedFile}s stored in this {@link BuildInfo}. */ public Collection<VersionedFile> getFiles(); /** * Helper method to retrieve a file version with given name. * @param name * @return the image version or <code>null</code> if not found */ public String getVersion(String name); /** * Stores an file with given name in this build info. * * @param name the unique name of the file * @param file the local {@link File} * @param version the file version */ public void setFile(String name, File file, String version); /** * Clean up any temporary build files */ public void cleanUp(); /** * Clones the {@link IBuildInfo} object. */ public IBuildInfo clone(); } ~~~ 该接口中定义了一些方法都是跟属性相关的,其实现在BuildInfo中。 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tradefed.build; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.MultiMap; import com.android.tradefed.util.UniqueMultiMap; import com.google.common.base.Objects; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Hashtable; import java.util.Map; /** * Generic implementation of a {@link IBuildInfo}. */ public class BuildInfo implements IBuildInfo { private String mBuildId = "0"; private String mTestTag = "stub"; private String mBuildTargetName = "stub"; private final UniqueMultiMap<String, String> mBuildAttributes = new UniqueMultiMap<String, String>(); private Map<String, VersionedFile> mVersionedFileMap; private String mBuildFlavor = null; private String mBuildBranch = null; private String mDeviceSerial = null; /** * Creates a {@link BuildInfo} using default attribute values. */ public BuildInfo() { mVersionedFileMap = new Hashtable<String, VersionedFile>(); } /** * Creates a {@link BuildInfo} * * @param buildId the build id * @param testTag the test tag name * @param buildTargetName the build target name */ public BuildInfo(String buildId, String testTag, String buildTargetName) { mBuildId = buildId; mTestTag = testTag; mBuildTargetName = buildTargetName; mVersionedFileMap = new Hashtable<String, VersionedFile>(); } /** * Creates a {@link BuildInfo}, populated with attributes given in another build. * * @param buildToCopy */ BuildInfo(BuildInfo buildToCopy) { this(buildToCopy.getBuildId(), buildToCopy.getTestTag(), buildToCopy.getBuildTargetName()); addAllBuildAttributes(buildToCopy); try { addAllFiles(buildToCopy); } catch (IOException e) { throw new RuntimeException(e); } } /** * {@inheritDoc} */ @Override public String getBuildId() { return mBuildId; } /** * {@inheritDoc} */ @Override public String getTestTag() { return mTestTag; } /** * {@inheritDoc} */ @Override public String getDeviceSerial() { return mDeviceSerial; } /** * {@inheritDoc} */ @Override public Map<String, String> getBuildAttributes() { return mBuildAttributes.getUniqueMap(); } /** * {@inheritDoc} */ @Override public String getBuildTargetName() { return mBuildTargetName; } /** * {@inheritDoc} */ @Override public void addBuildAttribute(String attributeName, String attributeValue) { mBuildAttributes.put(attributeName, attributeValue); } /** * Helper method to copy build attributes, branch, and flavor from other build. */ protected void addAllBuildAttributes(BuildInfo build) { mBuildAttributes.putAll(build.getAttributesMultiMap()); setBuildFlavor(build.getBuildFlavor()); setBuildBranch(build.getBuildBranch()); } protected MultiMap<String, String> getAttributesMultiMap() { return mBuildAttributes; } /** * Helper method to copy all files from the other build. * <p> * Creates new hardlinks to the files so that each build will have a unique file path to the * file. * </p> * * @throws IOException if an exception is thrown when creating the hardlinks. */ protected void addAllFiles(BuildInfo build) throws IOException { for (Map.Entry<String, VersionedFile> fileEntry : build.getVersionedFileMap().entrySet()) { File origFile = fileEntry.getValue().getFile(); File copyFile; if (origFile.isDirectory()) { copyFile = FileUtil.createTempDir(fileEntry.getKey()); FileUtil.recursiveHardlink(origFile, copyFile); } else { // Only using createTempFile to create a unique dest filename copyFile = FileUtil.createTempFile(fileEntry.getKey(), FileUtil.getExtension(origFile.getName())); copyFile.delete(); FileUtil.hardlinkFile(origFile, copyFile); } setFile(fileEntry.getKey(), copyFile, fileEntry.getValue().getVersion()); } } protected Map<String, VersionedFile> getVersionedFileMap() { return mVersionedFileMap; } /** * {@inheritDoc} */ @Override public File getFile(String name) { VersionedFile fileRecord = mVersionedFileMap.get(name); if (fileRecord != null) { return fileRecord.getFile(); } return null; } /** * {@inheritDoc} */ @Override public Collection<VersionedFile> getFiles() { return mVersionedFileMap.values(); } /** * {@inheritDoc} */ @Override public String getVersion(String name) { VersionedFile fileRecord = mVersionedFileMap.get(name); if (fileRecord != null) { return fileRecord.getVersion(); } return null; } /** * {@inheritDoc} */ @Override public void setFile(String name, File file, String version) { if (mVersionedFileMap.containsKey(name)) { CLog.e("Device build already contains a file for %s in thread %s", name, Thread.currentThread().getName()); return; } mVersionedFileMap.put(name, new VersionedFile(file, version)); } /** * {@inheritDoc} */ @Override public void cleanUp() { for (VersionedFile fileRecord : mVersionedFileMap.values()) { FileUtil.recursiveDelete(fileRecord.getFile()); } mVersionedFileMap.clear(); } /** * {@inheritDoc} */ @Override public IBuildInfo clone() { BuildInfo copy = new BuildInfo(mBuildId, mTestTag, mBuildTargetName); copy.addAllBuildAttributes(this); try { copy.addAllFiles(this); } catch (IOException e) { throw new RuntimeException(e); } copy.setBuildBranch(mBuildBranch); copy.setBuildFlavor(mBuildFlavor); return copy; } /** * {@inheritDoc} */ @Override public String getBuildFlavor() { return mBuildFlavor; } /** * {@inheritDoc} */ @Override public void setBuildFlavor(String buildFlavor) { mBuildFlavor = buildFlavor; } /** * {@inheritDoc} */ @Override public String getBuildBranch() { return mBuildBranch; } /** * {@inheritDoc} */ @Override public void setBuildBranch(String branch) { mBuildBranch = branch; } /** * {@inheritDoc} */ @Override public void setDeviceSerial(String serial) { mDeviceSerial = serial; } /** * {@inheritDoc} */ @Override public int hashCode() { return Objects.hashCode(mBuildAttributes, mBuildBranch, mBuildFlavor, mBuildId, mBuildTargetName, mTestTag, mDeviceSerial); } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } BuildInfo other = (BuildInfo) obj; return Objects.equal(mBuildAttributes, other.mBuildAttributes) && Objects.equal(mBuildBranch, other.mBuildBranch) && Objects.equal(mBuildFlavor, other.mBuildFlavor) && Objects.equal(mBuildId, other.mBuildId) && Objects.equal(mBuildTargetName, other.mBuildTargetName) && Objects.equal(mTestTag, other.mTestTag) && Objects.equal(mDeviceSerial, other.mDeviceSerial); } } ~~~ 提供的属性有build的id号,build的目标名称,测试标签,根目录,build的分支,测试类型。关机要理解build是什么意思,我还不是太了解build这个意义,暂时先用build来代替吧,等我了解了,再做解释。在原生的cts中,使用的是CtsBuildProvider,很简单的就是把cts的根目录属性设置上,然后是buildId,测试目标,build命令设置好就返回了。但是你如果要添加你自己的实现类,肯定不是简单的这么一点代码。比如你要做的测试时测试你的系统,那么这里面提供的东西就多了。首先你的系统存放的地址,分支号,要build的版本号等等都要在这里面获取并存到buildinfo对象中返回给TestInvocation中。 ~~~ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cts.tradefed.build; import com.android.tradefed.build.FolderBuildInfo; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.build.IBuildProvider; import com.android.tradefed.build.IFolderBuildInfo; import com.android.tradefed.config.Option; import java.io.File; /** * A simple {@link IBuildProvider} that uses a pre-existing CTS install. */ public class CtsBuildProvider implements IBuildProvider { @Option(name="cts-install-path", description="the path to the cts installation to use") private String mCtsRootDirPath = System.getProperty("CTS_ROOT"); public static final String CTS_BUILD_VERSION = "4.4_r1.95"; /** * {@inheritDoc} */ @Override public IBuildInfo getBuild() { if (mCtsRootDirPath == null) { throw new IllegalArgumentException("Missing --cts-install-path"); } IFolderBuildInfo ctsBuild = new FolderBuildInfo(CTS_BUILD_VERSION, "cts", "cts"); ctsBuild.setRootDir(new File(mCtsRootDirPath)); return ctsBuild; } /** * {@inheritDoc} */ @Override public void buildNotTested(IBuildInfo info) { // ignore } /** * {@inheritDoc} */ @Override public void cleanUp(IBuildInfo info) { // ignore } } ~~~
';

(7)-任务执行的调度室

最后更新于:2022-04-01 06:54:35

# TestInvocation ~~~ /** * {@inheritDoc} */ @Override public void invoke(ITestDevice device, IConfiguration config, IRescheduler rescheduler) throws DeviceNotAvailableException, Throwable { try { mStatus = "fetching build"; config.getLogOutput().init(); getLogRegistry().registerLogger(config.getLogOutput()); IBuildInfo info = null; if (config.getBuildProvider() instanceof IDeviceBuildProvider) { info = ((IDeviceBuildProvider) config.getBuildProvider()).getBuild(device); } else if (config.getBuildProvider() instanceof IDeviceConfigBuildProvider) { // 调用config获得cts.xml文件中的<build_provider>标签中对应的类,然后通过调用getBuild得到IBuildInfo对象 info = ((IDeviceConfigBuildProvider) config.getBuildProvider()).getBuild(device, config); } else { info = config.getBuildProvider().getBuild(); } if (info != null) { // System.out.println(String.format("setup: %s tearDown: %s", // config.getCommandOptions().isNeedPrepare(), // config.getCommandOptions().isNeedTearDown())); CLog.logAndDisplay(LogLevel.INFO, String.format("setup: %s tearDown: %s", config.getCommandOptions().isNeedPrepare(), config.getCommandOptions().isNeedTearDown())); // 获取<test>配置项里的测试选项,并注入到info中 injectBuild(info, config.getTests()); if (shardConfig(config, info, rescheduler)) { CLog.i("Invocation for %s has been sharded, rescheduling", device.getSerialNumber()); } else { device.setRecovery(config.getDeviceRecovery()); // 准备刷机,启动case performInvocation(config, device, info, rescheduler); // exit here, depend on performInvocation to deregister // logger return; } } else { mStatus = "(no build to test)"; CLog.d("No build to test"); rescheduleTest(config, rescheduler); } } catch (BuildRetrievalError e) { CLog.e(e); /* * because this is BuildRetrievalError, so do not generate test * result // report an empty invocation, so this error is sent to * listeners startInvocation(config, device, e.getBuildInfo()); // * don't want to use #reportFailure, since that will call * buildNotTested for (ITestInvocationListener listener : * config.getTestInvocationListeners()) { * listener.invocationFailed(e); } reportLogs(device, * config.getTestInvocationListeners(), config.getLogOutput()); * InvocationSummaryHelper.reportInvocationEnded( * config.getTestInvocationListeners(), 0); return; */ } catch (IOException e) { CLog.e(e); } // save current log contents to global log getLogRegistry().dumpToGlobalLog(config.getLogOutput()); getLogRegistry().unregisterLogger(); config.getLogOutput().closeLog(); } ~~~ 之所以称为调度室,因为它就是去一步一步的运行9大组件的剩余组件,组件与组件之间并不知道对方的存在,只有TestInvocation自己知道。它通过反射的机制得到对象,分别创建对象,然后调用其中的接口方法得到它想要的,然后再传给下一个组件。以上方法就可以看出,它先得到buildprovider对象。调用getBuild对象得到IBuildInfo对象。咱们这个程序中,其实就是调用了CtsBuildProvider的getBuild方法。 如果info不为null:然后给测试任务注入IBuildInfo,判断是否可以分拆任务,如果不能分拆的话,注册设备恢复类,然后跳转到performInvocation方法中。 如果info为null:提示没有可执行的build,调用IRescheduler.rescheduleCommand()重试。 ~~~ /** * Performs the invocation * * @param config * the {@link IConfiguration} * @param device * the {@link ITestDevice} to use. May be <code>null</code> * @param info * the {@link IBuildInfo} */ private void performInvocation(IConfiguration config, ITestDevice device, IBuildInfo info, IRescheduler rescheduler) throws Throwable { boolean resumed = false; long startTime = System.currentTimeMillis(); long elapsedTime = -1; info.setDeviceSerial(device.getSerialNumber()); startInvocation(config, device, info); try { device.setOptions(config.getDeviceOptions()); // 准备build和跑case prepareAndRun(config, device, info, rescheduler); } catch (BuildError e) { CLog.w("Build %s failed on device %s. Reason: %s", info.getBuildId(), device.getSerialNumber(), e.toString()); takeBugreport(device, config.getTestInvocationListeners(), BUILD_ERROR_BUGREPORT_NAME); reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler); } catch (TargetSetupError e) { CLog.e("Caught exception while running invocation"); CLog.e(e); reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler); // maybe test device if offline, check it device.waitForDeviceOnline(); } catch (DeviceNotAvailableException e) { // log a warning here so its captured before reportLogs is called CLog.w("Invocation did not complete due to device %s becoming not available. " + "Reason: %s", device.getSerialNumber(), e.getMessage()); if ((e instanceof DeviceUnresponsiveException) && TestDeviceState.ONLINE.equals(device.getDeviceState())) { // under certain cases it might still be possible to grab a // bugreport takeBugreport(device, config.getTestInvocationListeners(), DEVICE_UNRESPONSIVE_BUGREPORT_NAME); } resumed = resume(config, info, rescheduler, System.currentTimeMillis() - startTime); if (!resumed) { reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler); } else { CLog.i("Rescheduled failed invocation for resume"); } throw e; } catch (RuntimeException e) { // log a warning here so its captured before reportLogs is called CLog.w("Unexpected exception when running invocation: %s", e.toString()); reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler); throw e; } catch (AssertionError e) { CLog.w("Caught AssertionError while running invocation: ", e.toString()); reportFailure(e, config.getTestInvocationListeners(), config, info, rescheduler); } finally { mStatus = "done running tests"; try { // reportLogs(device, config.getTestInvocationListeners(), config.getLogOutput()); elapsedTime = System.currentTimeMillis() - startTime; if (!resumed) { // 发送报告 InvocationSummaryHelper.reportInvocationEnded(config.getTestInvocationListeners(), elapsedTime); } } finally { config.getBuildProvider().cleanUp(info); } } } ~~~ 首先启动所有监听器的start方法,起一个通知开始的作用。 ~~~ /** * Starts the invocation. * <p/> * Starts logging, and informs listeners that invocation has been started. * * @param config * @param device * @param info */ private void startInvocation(IConfiguration config, ITestDevice device, IBuildInfo info) { logStartInvocation(info, device); for (ITestInvocationListener listener : config.getTestInvocationListeners()) { try { listener.invocationStarted(info); } catch (RuntimeException e) { // don't let one listener leave the invocation in a bad state CLog.e("Caught runtime exception from ITestInvocationListener"); CLog.e(e); } } } ~~~ 然后得到device_options对象,设置到ITestDevice中。再调用prepareAndRun(config, device, info, rescheduler);方法。 ~~~ private void prepareAndRun(IConfiguration config, ITestDevice device, IBuildInfo info, IRescheduler rescheduler) throws Throwable { // use the JUnit3 logic for handling exceptions when running tests Throwable exception = null; try { // if (config.getCommandOptions().isNeedPrepare()&&!isRepeat) { doSetup(config, device, info); //下次启动的时候,不再刷机 isRepeat = true; }else{ CLog.logAndDisplay(LogLevel.DEBUG, String.format("No need to flash,derect to run case")); } // 跑case runTests(device, info, config, rescheduler); } catch (Throwable running) { exception = running; } finally { try { if (config.getCommandOptions().isNeedTearDown()) { doTeardown(config, device, info, exception); } } catch (Throwable tearingDown) { if (exception == null) { exception = tearingDown; } } } if (exception != null) { throw exception; } } ~~~ 在该方法中先得到根据cmd_options得到是否需要prepare,如果需要,就调用target_prepare类来做准备工作,如果不需要,直接调用runTests方法 ~~~ private void runTests(ITestDevice device, IBuildInfo buildInfo, IConfiguration config, IRescheduler rescheduler) throws DeviceNotAvailableException { List<ITestInvocationListener> listeners = config.getTestInvocationListeners(); for (IRemoteTest test : config.getTests()) { if (test instanceof IDeviceTest) { ((IDeviceTest) test).setDevice(device); } test.run(new ResultForwarder(listeners)); } } ~~~ 上面的方法调用tests配置的类来追个调用里面的run方法启动测试。测试完成后,来做tearDown的工作。
';

(6)-任务的执行

最后更新于:2022-04-01 06:54:33

前两篇讲了任务的添加和9大项配置,这篇讲任务的执行。 # 任务的执行 任务的执行在CommandScheduler的run方法中,所以删除所有的断点,在run方法中打上断点,重启启动debug: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dce563b8.jpg) 先看while循环下面的第一行代码 ~~~ ExecutableCommand cmd = dequeueConfigCommand(); ~~~ ~~~ private ExecutableCommand dequeueConfigCommand() { try { // poll for a command, rather than block indefinitely, to handle shutdown case return mCommandQueue.poll(getCommandPollTimeMs(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { CLog.i("Waiting for command interrupted"); } return null; } ~~~ 从队列中取出第一个对象。如果队列中没有元素那就返回null。返回后,while中会判断如果为null的话,就会结束再次调用 ~~~ ExecutableCommand cmd = dequeueConfigCommand(); ~~~ 直到cmd不为null。所以在 ~~~ IDeviceSelection options = cmd.getConfiguration().getDeviceRequirements(); ~~~ 打上断点,按F8,程序会在cmd不为null时进入到上面的代码,停下来。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcea9e22.jpg) 首先得到系统设备要求,根据该要求,判断是否有满足要求的设备并分配该设备。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcedbbb4.jpg) 原生的 要求就一个S/N号,ddms会根据该SN号分配一个IDevice,然后cts根据IDevice包装成ITestDevice对象返回。然后根据ITestDevice,IDeviceManager和ExecutableCommand开始真正的启动任务。我们先不急于讨论执行的过程,而是继续向下看: ~~~ addInvocationThread(invThread); if (cmd.isLoopMode()) { addNewExecCommandToQueue(cmd.getCommandTracker()); } ~~~ 先将正在执行的线程存到set中,然后将该命令再次放入任务队列中,这样的目的是为了能循环执行该任务。如果根据sn号没有分配成功ITestDevice,则会再次尝试一次,如果在规定时间内还没找到设备则放弃。最后做一些tearDown操作,然后将case运行过程中的log保存到文件。就算执行完了。 现在回头看一下执行任务的线程中是如何操作的: ~~~ private InvocationThread startInvocation(IDeviceManager manager, ITestDevice device, ExecutableCommand cmd) { final String invocationName = String.format("Invocation-%s", device.getSerialNumber()); InvocationThread invocationThread = new InvocationThread(invocationName, manager, device, cmd); invocationThread.start(); return invocationThread; } ~~~ InvocationThread为CommandScheduler私有内部类,继承与线程,这就属于线程里启动线程。所以直接看InvocationThread的run方法就ok了。在run方法里调用了ITestInvocation的invoke方法: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcf07e9c.jpg) 传入的对象分别为ITestDevice、IConfiguration、IRescheduler。前两个已经涉及到,最后一个IRescheduler是什么?自己看吧,也没啥意义。这样任务的前期的工作都已经搞定了,程序转到了TestInvocation的invoke方法。放到下一篇文章来讲,因为它很重要,相当于一个调度室的作用。 # 总结 写了6篇,总结一些cts的过程。 任务的开始点是接受命令行中的参数,如果判断为执行cts任务,那么会在Console.run方法中启动添加命令任务的线程**ArgRunnable**和执行任务的线程**CommandScheduler**。添加任务线程**ArgRunnable**会调用**CommandScheduler**对象方法addCommand来向**ConditionPriorityBlockingQueue(采用先进先出的队列)**这个队列中添加,而执行任务线程**CommandScheduler**会通过调用自己的私有方法dequeueConfigCommand来从 **ConditionPriorityBlockingQueue**去取可执行的任务,每次取第一个。然后调用**InvocationThread**这个线程去执行任务。**InvocationThread**这个线程调用TestInvocation.invoke方法去执行任务。 明白了么,其实很简单!![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcf36bd4.gif)
';

(5)-9大组件配置

最后更新于:2022-04-01 06:54:31

# 解析配置文件 Cts框架分为9大部分: cmd_options:命令行接受的参数选项,command包中。 device_requirements:设备相关要求,device包中 device_options:设备参数要求,device包中 builde_provider:版本提供者,build包中 target_preparer:预置条件准备,targetprep包中 test:测试类型,存在testtype包中 device_recovery:任务执行过程中设备异常后的设备恢复,device包中 logger:日志系统,log包中 result_reporter:结果统计报告,result包中 每一种任务都要配置好这9个组件,如果不配置,框架就采用自己的默认配置,但也是相当于配置了这几项,所以cts框架的核心在Configuration中。 ~~~ private static synchronized Map<String, ObjTypeInfo> getObjTypeMap() { if (sObjTypeMap == null) { sObjTypeMap = new HashMap<String, ObjTypeInfo>(); sObjTypeMap.put(BUILD_PROVIDER_TYPE_NAME, new ObjTypeInfo(IBuildProvider.class, false)); sObjTypeMap.put(TARGET_PREPARER_TYPE_NAME, new ObjTypeInfo(ITargetPreparer.class, true)); sObjTypeMap.put(TEST_TYPE_NAME, new ObjTypeInfo(IRemoteTest.class, true)); sObjTypeMap.put(DEVICE_RECOVERY_TYPE_NAME, new ObjTypeInfo(IDeviceRecovery.class, false)); sObjTypeMap.put(LOGGER_TYPE_NAME, new ObjTypeInfo(ILeveledLogOutput.class, false)); sObjTypeMap.put(RESULT_REPORTER_TYPE_NAME, new ObjTypeInfo(ITestInvocationListener.class, true)); sObjTypeMap.put(CMD_OPTIONS_TYPE_NAME, new ObjTypeInfo(ICommandOptions.class, false)); sObjTypeMap.put(DEVICE_REQUIREMENTS_TYPE_NAME, new ObjTypeInfo(IDeviceSelection.class, false)); sObjTypeMap.put(DEVICE_OPTIONS_TYPE_NAME, new ObjTypeInfo(TestDeviceOptions.class, false)); } return sObjTypeMap; } /** * Creates an {@link Configuration} with default config objects. */ public Configuration(String name, String description) { mName = name; mDescription = description; mConfigMap = new LinkedHashMap<String, List<Object>>(); setCommandOptions(new CommandOptions()); setDeviceRequirements(new DeviceSelectionOptions()); setDeviceOptions(new TestDeviceOptions()); setBuildProvider(new StubBuildProvider()); setTargetPreparer(new StubTargetPreparer()); setTest(new StubTest()); setDeviceRecovery(new WaitDeviceRecovery()); setLogOutput(new StdoutLogger()); setTestInvocationListener(new TextResultReporter()); } ~~~ 在getObjTypeMap()可以看出来cts框架为这9个组件定义的接口,只要你的类实现了这个接口,cts就可以通过反射机制找到你的类。Configuration类的构造方法中设置了这几个接口默认的实现类,如果你没有配置其他的替代类,cts会默认去加载这些实现类。这些类都是实现了上面方法中对应关系的接口,才能被设置成配置项的。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcb19de0.jpg) 所有关于类的配置,cts只认你在Configuration类接口的实现类,你如果写了一个类没有继承9大类中的一个接口,你添加了,也会报错的。从上图可以看出,cts默认配置了上面几项,没有配置的采用默认的。这个讲完了,我们就从文章开头的代码开始将它是如何一步一步完成这些配置的。 # Debug 以[上一篇文章](http://blog.csdn.net/itfootball/article/details/40210811)中的解析配置文件代码开始: ~~~ IConfiguration config = getConfigFactory().createConfigurationFromArgs(args); ~~~ ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcb6d18c.jpg) 调用的是ConfigurationFactory.createConfigurationFromArgs方法: ~~~ /** * {@inheritDoc} */ @Override public IConfiguration createConfigurationFromArgs(String[] arrayArgs) throws ConfigurationException { List<String> listArgs = new ArrayList<String>(arrayArgs.length); IConfiguration config = internalCreateConfigurationFromArgs(arrayArgs, listArgs); config.setOptionsFromCommandLineArgs(listArgs); return config; } ~~~ 该方法又调用了internalCreateConfigurationFromArgs方法,传入的是参数run cts --plan Signature字符串数组和一个空字符串。 ~~~ private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs, List<String> optionArgsRef) throws ConfigurationException { if (arrayArgs.length == 0) { throw new ConfigurationException("Configuration to run was not specified"); } optionArgsRef.addAll(Arrays.asList(arrayArgs)); // first arg is config name final String configName = optionArgsRef.remove(0); ConfigurationDef configDef = getConfigurationDef(configName, false); return configDef.createConfiguration(); } ~~~ 该方法中取出第一个参数cts然后传入getConfigurationDef方法中,获取ConfigurationDef对象。首先看看ConfigurationDef对象是什么?很简单,一个ConfigurationDef代表一个配置文件。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcc2d3ea.jpg) ~~~ public class ConfigurationDef { /** a map of object type names to config object class name(s). */ private final Map<String, List<String>> mObjectClassMap; /** a list of option name/value pairs. */ private final List<OptionDef> mOptionList; /** a cache of the frequency of every classname */ private final Map<String, Integer> mClassFrequency; static class OptionDef { final String name; final String key; final String value; OptionDef(String optionName, String optionValue) { this(optionName, null, optionValue); } OptionDef(String optionName, String optionKey, String optionValue) { this.name = optionName; this.key = optionKey; this.value = optionValue; } } /** the unique name of the configuration definition */ private final String mName; /** a short description of the configuration definition */ private String mDescription = ""; public ConfigurationDef(String name) { mName = name; // use LinkedHashMap to keep objects in same order they were added. mObjectClassMap = new LinkedHashMap<String, List<String>>(); mOptionList = new ArrayList<OptionDef>(); mClassFrequency = new HashMap<String, Integer>(); } ~~~ 主要关注下面2个属性: mObjectClassMap:保存9大组件的,例如上面的build_provider、device_recovery、test、logger、result_reporter这些标签对应的类都保持该map对象中。 mOptionList:保存option标签的值,例如上面的enable-root的值。 现在进入debug模式验证一下是不是用上面两个属性保存的。将断点打在ConfigurationDef configDef = getConfigurationDef(configName, false);上,删除其他断点。重启debug。按F6跳到下一行,来看一下Variables一栏中值: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcc7f3c3.jpg) 可以看到mObjectClassMap的值为: ~~~ {build_provider=[com.android.cts.tradefed.build.CtsBuildProvider], device_recovery=[com.android.tradefed.device.WaitDeviceRecovery], test=[com.android.cts.tradefed.testtype.CtsTest], logger=[com.android.tradefed.log.FileLogger], result_reporter=[com.android.cts.tradefed.result.CtsXmlResultReporter, com.android.cts.tradefed.result.IssueReporter]} ~~~ 和配置文件里的是一样的,mOptionList也保存了xml中option对应的值: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcd00312.jpg) 然后通过configDef.createConfiguration();来创建Configuration对象。所以我们进入该方法它是如何将原生的替换为cts.xml中配置的: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcd2b073.jpg) 将debug点跳转到for循环中,然后在Variables一栏中分别查看this中的mObjectClassMap和config中mConfigMap的值: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcd94dd0.jpg)![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcdc1200.jpg) 可以看到mConfigMap是原生的,也就是Configuration的构造方法中添加的接口类,mObjectClassMap保存就是cts.xml配置文件里的接口类。然后根据Map的key值不能重复的特性用cts.xml配置的接口类替换掉原生的接口类。所以把断点打在返回语句return上,按F8看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-09_56911dcdeedb8.jpg) 这个时候可以看到mConfigMap的值变了: ~~~ {cmd_options=[com.android.tradefed.command.CommandOptions@7b2390], device_requirements=[com.android.tradefed.device.DeviceSelectionOptions@5bd18b], device_options=[com.android.tradefed.device.TestDeviceOptions@1a29fe], target_preparer=[com.android.tradefed.targetprep.StubTargetPreparer@1beaa92], build_provider=[com.android.cts.tradefed.build.CtsBuildProvider@c4f317], device_recovery=[com.android.tradefed.device.WaitDeviceRecovery@33967b], test=[com.android.cts.tradefed.testtype.CtsTest@7263d0], logger=[com.android.tradefed.log.FileLogger@1a7422e], result_reporter=[com.android.cts.tradefed.result.CtsXmlResultReporter@1750ae1, com.android.cts.tradefed.result.IssueReporter@b279f3]} ~~~ 9大项全部配置完成。
';