【谷歌G认证-XTS问题整理】CTS-testManagedContactsUris预置联系人相关

771 阅读11分钟

忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。

                        -- 服装学院的IT男

1. 错误描述

新项目首轮全册出现CTS fail。

测试模块:CtsDevicePolicyManagerTestCases 测试case: com.android.cts.devicepolicy.ManagedProfileContactsTest#testManagedContactsUris com.android.cts.devicepolicy.ManagedProfileContactsTest#testManagedContactsPolicies

testManagedContactsUris错误信息.png

1.1 错误信息提取

错误信息堆栈很长,而且尝试从 testManagedContactsUris 方法开始分析的时候发现代码很多,难度很大。所以决定从报错的后面堆栈开始分析。

根据截图里的这段错误信息知道是在 testManagedProfileContactList_canListManagedContacts 开始出现断言失败,失败原因:期望返回2,结果返回 4

junit.framework.AssertionFailedError: expected:<2> but was:<4>
	at junit.framework.Assert.fail(Assert.java:50)
	at junit.framework.Assert.failNotEquals(Assert.java:287)
	at junit.framework.Assert.assertEquals(Assert.java:67)
	at junit.framework.Assert.assertEquals(Assert.java:199)
	at junit.framework.Assert.assertEquals(Assert.java:205)
	at com.android.cts.managedprofile.ContactsTest.testManagedProfileContactList_canListManagedContacts(ContactsTest.java:340)

2. 测试代码分析(第一轮)

直接从 ContactsTest 类下的 testManagedProfileContactList_canListManagedContacts 方法开始看

# ContactsTest

    public void testManagedProfileContactList_canListManagedContacts() {
        assertTrue(isManagedProfile());
        // 获取集合
        ArrayList<ContactInfo> contacts = getContactInfoFromListContacts(false /*isEnterprise*/);
        // 错误信息报错行 
        assertEquals(2, contacts.size());
        assertEquals(MANAGED_CONTACT_NAMES, mapToDisplayNames(contacts));
        // When the managed profile queries itself, all contacts should have "normal" IDs
        assertFalse(isEnterpriseContactId(contacts.get(0).contactId));
        assertFalse(isEnterpriseContactId(contacts.get(1).contactId));
    }

根据错误信息指向的是 “assertEquals(2, contacts.size());” 这一行。

看到这行说明方向是正确的,这里期望值是 2 ,但是 “contacts.size()” 的长度是 4 , 所以断言失败,导致测试失败。

而这个集合的返回来自 getContactInfoFromListContacts 方法,这边传递进去的参数为 false

# ContactsTest

    private ArrayList<ContactInfo> getContactInfoFromListContacts(boolean isEnterprise) {
        // 为false,所以取的是 Contacts.CONTENT_URI
        Uri uri = isEnterprise ? Contacts.ENTERPRISE_CONTENT_URI : Contacts.CONTENT_URI;
        return getAllContactsFromUri(uri, Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY,
                Contacts.PHOTO_URI, Contacts.PHOTO_THUMBNAIL_URI, Contacts.PHOTO_ID);
    }

继续执行 getAllContactsFromUri ,这里的几个参数可以看到都是定义在 Contacts 下的一些常量,这些值感兴趣的可以自己看看,不知道也没事,不影响最终分析。

# ContactsTest
    private ContentResolver mResolver;

    private ArrayList<ContactInfo> getAllContactsFromUri(Uri uri, String idColumn,
            String displayNameColumn, String photoUriColumn, String photoThumbnailColumn,
            String photoIdColumn) {
        Cursor cursor = mResolver.query(uri,
                new String[] {
                        idColumn,
                        displayNameColumn,
                        photoUriColumn,
                        photoIdColumn,
                        photoThumbnailColumn,
                }, null, null, null);
        try (cursor) {
            ArrayList<ContactInfo> allContacts = new ArrayList<>();
            while (cursor != null && cursor.moveToNext()) {
                allContacts.add(new ContactInfo(
                        cursor.getString(cursor.getColumnIndexOrThrow(idColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(displayNameColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoUriColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoThumbnailColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoIdColumn))));
            }
            return allContacts;
        }
    }
    1. 通过 ContentResolver 查询数据
    1. 定义一个返回集合 allContacts,内部存放 ContactInfo
    1. 把查询到的数据构建出一个 ContactInfo ,然后添加到返回值 allContacts 中
    1. 返回 allContacts

代码逻辑不复杂,测试逻辑到这其实就已经知道测试失败更具体的原因了,根据信息查询联系人,预期结果是查询到2个,但是实际上返回了4个。

知道问题fail根因后,下面要做的事情如下;

    1. 这里的结果是是哪 4 个联系人
    1. 哪2个是正确的,哪2个是多出来的
    1. 多出来的那2个联系人是哪来的
    1. 删除多出来的联系人,复测看是否能pass

3. 问题处理(第一轮)

3.1 在测试逻辑上加上log

返回的集合里放的是 ContactInfo ,这是在当前 ContactsTest 类下面定义的一个数据结构,先给他加上 toString ,为了便于看打印。

# ContactsTest
    private class ContactInfo { // Not static to access outer world.

        String contactId;
        String displayName;
        String photoUri;
        String photoThumbnailUri;
        String photoId;
        ......// 省略其他方法

        // 加上toString
        public String toString() {
			return "ContactInfo{"
					+ "contactId=" + contactId
					+ ", displayName=" + displayName
					+ ", photoUri=" + photoUri
					+ ", photoThumbnailUri= " + photoThumbnailUri
					+ ", photoId=" + photoId
					+ '}';
		}
    }

然后在测试代码加上log,修改后代码如下:

# ContactsTest
    private ContentResolver mResolver;

    private ArrayList<ContactInfo> getAllContactsFromUri(Uri uri, String idColumn,
            String displayNameColumn, String photoUriColumn, String photoThumbnailColumn,
            String photoIdColumn) {
        Cursor cursor = mResolver.query(uri,
                new String[] {
                        idColumn,
                        displayNameColumn,
                        photoUriColumn,
                        photoIdColumn,
                        photoThumbnailColumn,
                }, null, null, null);
        try (cursor) {
            ArrayList<ContactInfo> allContacts = new ArrayList<>();
            while (cursor != null && cursor.moveToNext()) {
                // 1. 构建数据的地方提出来
				ContactInfo info = new ContactInfo(
                        cursor.getString(cursor.getColumnIndexOrThrow(idColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(displayNameColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoUriColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoThumbnailColumn)),
                        cursor.getString(cursor.getColumnIndexOrThrow(photoIdColumn)));
                // 2. 加上log
				Log.i("biubiubiu", "allContacts.add "+info.toString());
                // 3. 保持原逻辑,添加到集合
                allContacts.add(info);
            }
            return allContacts;
        }
    }

目的就是把这个4个元素都打印出来,看看到底查出来哪4个联系人。

3.2 分析日志

把编译出来的文件替换后重新跑测试,获取 log 并分析

llContacts.add ContactInfo{contactId=1, displayName=XXXX Service Contacts, photoUri=null, photoThumbnailUri= null, photoId=null}
allContacts.add ContactInfo{contactId=2, displayName=XXXX Service Contacts, photoUri=null, photoThumbnailUri= null, photoId=null}
allContacts.add ContactInfo{contactId=3, displayName=Managed, photoUri=content://com.android.contacts/contacts/3/photo, photoThumbnailUri= content://com.android.contacts/contacts/3/photo, photoId=18}
allContacts.add ContactInfo{contactId=4, displayName=ManagedShared, photoUri=null, photoThumbnailUri= null, photoId=null}

可以看到确实是有4个数据,并且根据pass的设备对比,知道第3,4这两个联系人是期望的结果,前面2个联系人是多出来的。

前面的2个联系人数据由于项目原因,用“XXXX Service Contacts”代替。

3.3 解决方案(第一轮)

已经知道多出来的2个联系人的名字了,就根据信息去项目代码里找添加的地方。这一步如果不确定可以问公司负责 Contact 模块的人,为什么多出来2个预置的联系人。 我这边在项目的 Contact 目录下 grep 命令找到了预置这2个地方。然后将其移除后单编push,进行再跑 CTS 验证,结果为pass

本以为问题已经得到解决,于是找到Contact Owner 进行沟通,但是对方反馈预置联系人需求很正常,而且其他项目都有,但是 CTS 都是 pass 的,于是找了个也有预置联系人的项目验证,确实如此,所以还需要继续分析。

4. 测试代码分析(第二轮)

首先可以确认报错的原因还是测试用例查到的联系人多出来了2个预置的。 但是因为其他项目也预置了联系人,也能 pass ,那解决方案肯定不是把这个预置联系人的需求删除。在之前分析测试代码的时候是忽略了前面的调用链,直接从最后报错的地方开始看到,所以可能还忽略了其他的测试逻辑。

直接看后面这段报错的思路是没有问题的,也是最快速的,只不过当前 case 稍微复杂一点,但是也是在最快的视觉把问题的原因定位出来了。

现在分析一下前面的调用逻辑,这就要从 case 最开始执行的方法看了。

4.1 测试用例总结

testManagedContactsUris 这条 case 的代码还是很多的,根据方法名已经当前 fail 部分的分析,这条测试的目的还是很简单:

  1. 添加X个联系人
  2. 获取所有联系人
  3. 比较数量,是否一致

说到底就是我往联系人里添加了几个数据,我就希望查询到几个。

测试用例是通过 Content Provider 对数据库的增删改查来完成的。 带着这个宏观概念,开始梳理相关测试代码

4.2 分析

# ManagedProfileContactsTest

    public void testManagedContactsUris() throws Exception {
        runManagedContactsTest(() -> {
            ContactsTestSet contactsTestSet = new ContactsTestSet(ManagedProfileContactsTest.this,
                    MANAGED_PROFILE_PKG, mParentUserId, mProfileUserId);

            contactsTestSet.setCallerIdEnabled(true);
            contactsTestSet.setContactsSearchEnabled(true);
            // 本次分析的地方在这里,留个印象
            contactsTestSet.checkIfCanListEnterpriseContacts(true);
            contactsTestSet.checkIfCanLookupEnterpriseContacts(true);
            contactsTestSet.checkIfCanFilterEnterpriseContacts(true);
            contactsTestSet.checkIfCanFilterSelfContacts();
            return null;
        });
    }

这一看就是 Lambda 表达式的形式,先看一下 runManagedContactsTest 方法的定义

# ManagedProfileContactsTest
    private void runManagedContactsTest(Callable<Void> callable) throws Exception {
        try {
            // Allow cross profile contacts search.
            // TODO test both on and off.
            getDevice().executeShellCommand(
                    "settings put --user " + mProfileUserId
                    + " secure managed_profile_contact_remote_search 1");

            ......
            // Remove parent user's contacts to make sure the test starts from a clean state
            // 1. 删除联系人
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testRemoveAllUserContacts", mParentUserId);
            ......// 省略其他
            // 2. 执行Lambda 表达式的内容
            callable.call();

        } finally {
            // 3. 把测试用例添加的联系人删掉
            // Clean up in managed profile and primary profile
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testCurrentProfileContacts_removeContacts", mProfileUserId);
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testCurrentProfileContacts_removeContacts", mParentUserId);
            getDevice().uninstallPackage(DIRECTORY_PROVIDER_PKG);
        }
    }

testManagedContactsUris 这条 case 从这里看就感觉到方法很多,从上面的这段从的调用知道用例大的代码逻辑如下:

    1. runManagedContactsTest 方法先执行一堆初始化操作,其中有一个是执行 testRemoveAllUserContacts 方法先把联系人都删了
    1. 然后开始执行Lambda 表达式的部门,这里很多方法,都是具体的测试逻辑,其中会联系人有增删改查的操作,当前报错的是在 checkIfCanListEnterpriseContacts 方法内触发的
    1. 测试逻辑执行完后,把测试过程中添加的联系人都删除 (我觉得是想还原设备环境,但是只是删除了测试新增的,但是原来的联系人被删除了,没有加回来)

在测试命令端,和测试报告都可以看到以下输出:

testManagedContactsUris方法链.png

可以确认在执行到 testManagedProfileContactList_canListManagedContacts 方法前,确实执行了 testRemoveAllUserContacts 方法,这个加上了日志也确实把预置联系人都删除了。

# ContactsTest

    public void testRemoveAllUserContacts() throws RemoteException {
		Log.i("biubiubiu", "testRemoveAllUserContacts before=  "+getContactInfoFromListContacts(false).size());
        for (ContactInfo contact : getContactInfoFromListContacts(false)) {
            mResolver.delete(
                    Contacts.CONTENT_URI.buildUpon().appendPath(contact.contactId).build(), null);
        }
		Log.i("biubiubiu", "testRemoveAllUserContacts after=  "+getContactInfoFromListContacts(false).size());
        assertEquals(0, getContactInfoFromListContacts(false).size());
    }

在首次执行case的时候,设备上有2个预置联系人,所以测试执行到这里时,先输出size为2,然后删除后就输出size为0 。

从这一段代码看,需求上预置联系人是不会导致 fail 的,因为测试前会执行删除。

在看报告中测试方法的执行顺序,最后一个 pass 的是 testPrimaryProfilePhoneList_canListOnlyPrimaryContacts 方法,看一下这个方法:

# ContactsTest

    public void testPrimaryProfilePhoneList_canListOnlyPrimaryContacts() {
        assertFalse(isManagedProfile());
        // 注意这里是 getContactInfoFromListPhones
        ArrayList<ContactInfo> contacts = getContactInfoFromListPhones(false /*isEnterprise*/);
        assertEquals(2, contacts.size());
        ......
    }

这个方法执行的是 getContactInfoFromListPhones 方法,和报错执行的不一样,所以再往上找。

# ContactsTest
    public void testPrimaryProfileContactList_canListOnlyPrimaryContacts() {
        assertFalse(isManagedProfile());
        ArrayList<ContactInfo> contacts = getContactInfoFromListContacts(false /*isEnterprise*/);
		Log.d("biubiubiu", "testPrimaryProfileContactList_canListOnlyPrimaryContacts befor "+ contacts.size());
        assertEquals(2, contacts.size());
        assertEquals(PRIMARY_CONTACT_NAMES, mapToDisplayNames(contacts));
        assertFalse(isEnterpriseContactId(contacts.get(0).contactId));
        assertFalse(isEnterpriseContactId(contacts.get(1).contactId));
		Log.d("biubiubiu", "testPrimaryProfileContactList_canListOnlyPrimaryContacts end "+ contacts.size());
		
    }

这个方法也是通过 getContactInfoFromListContacts 来获取联系人数量的,参数也是fail。

而且这里的断言的数量也是 2 ,和当前报错的方法是一致的。

也就是说先执行 testPrimaryProfileContactList_canListOnlyPrimaryContacts 再执行 testManagedProfileContactList_canListManagedContacts , 2个方法都是一样的 API 去获取联系人数量,并且断言的数量都是2。

现在是前者获取2,测试pass, 后续获取到数量为4, 测试fail。

分析到这里,就很奇怪,如果说有问题,那就是在这2个方法之间,又有某个地方重新把2个联系人加回来了。

于是从这期间找代码,找了很久,甚至还怀疑是 Content Provider 查询除了问题,还直接导出数据库来确认。

最终发现这条路是错误的。

4.3 多用户场景测试

查询同一个数据库,不可能在数据库没有改变的情况下,查询到的数据不一样,所以原因肯定是:查询的不是同一个数据库。

但是用的是同一个方法查询,那查询的肯定是同一个数据库,这个想法没问题,但是忽略了一个场景:多用户

前面分析代码的时候忽略了各个方法中还传递了用户ID这个参数。测试类 ManagedProfileContactsTest 的父类是 BaseManagedProfileTest 在 BaseManagedProfileTest 下面定义了很多变量,其中就有用户ID。

# BaseManagedProfileTest
    // 主用户ID
    protected int mParentUserId;
    // 测试用户ID
    protected int mProfileUserId;

这2个用户ID的赋值逻辑还是有一部分代码的,当前不展开分析,感兴趣的可以自己看。

正常情况下,mParentUserId 就是主用户ID,ID = 0 , mProfileUserId 就是当前为了测试,新建的一个用户。

在 ManagedProfileContactsTest::runManagedContactsTest 方法会先执行 runDeviceTestsAsUser 来删除已有联系人,保证测试环境的“干净”。但是这个方法传递了一个用户ID 作为参数

# ManagedProfileContactsTest
    private void runManagedContactsTest(Callable<Void> callable) throws Exception {
            ......
            // Remove parent user's contacts to make sure the test starts from a clean state
            // 1. 删除联系人
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testRemoveAllUserContacts", mParentUserId);
            ......// 省略其他
            // 2. 执行Lambda 表达式的内容
            callable.call();
            ......

    }

注意这里传递的是 mParentUserId ,也就是说对主用户做了删除操作。

而 testManagedProfileContactList_canListManagedContacts 方法执行的时候,传递是子用户的 mProfileUserId

# ManagedProfileContactsTest
    public void checkIfCanListEnterpriseContacts(boolean expected)
            throws DeviceNotAvailableException {
        // Primary user with standard list api can only list primary contacts
        runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
                "testPrimaryProfileContactList_canListOnlyPrimaryContacts",
                mParentUserId);
        runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
                "testPrimaryProfilePhoneList_canListOnlyPrimaryContacts",
                mParentUserId);
        // Managed user with standard list api can only list managed contacts
        runDeviceTestsAsUser(mManagedProfilePackage, ".ContactsTest",
                "testManagedProfileContactList_canListManagedContacts",
                mProfileUserId);
        ......
    }

那么问题的本质就清晰了,删除的操作和查询的操作,在同一个用户下执行的,而用户直接数据是隔离的。所以当前 case fail 的原因是在新建用户下,多出来了2个预置联系人。

4.4 确认问题

前面是对代码的分析,根据代码的分析结果,

    1. 手动增加新用户并切换,观察是否有预置联系人。
    1. 在原套件代码增加对新增子用户的联系人删除,代码如下
# ManagedProfileContactsTest
    private void runManagedContactsTest(Callable<Void> callable) throws Exception {
            ......
            // Remove parent user's contacts to make sure the test starts from a clean state
            // 1. 删除联系人
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testRemoveAllUserContacts", mParentUserId);
            // 新增对子用户的联系人删除
            runDeviceTestsAsUser(MANAGED_PROFILE_PKG, ".ContactsTest",
                    "testRemoveAllUserContacts", mProfileUserId);
            ......// 省略其他
            // 2. 执行Lambda 表达式的内容
            callable.call();
            ......

    }

测试结果:

    1. CTS pass 的项目切换后没有预置联系人,但是当前设备切换用户就已经有了预置联系人
    1. 修改套件后 pass

因此确认当前 case fail 的原因就是新增子用户的时候,不应该把主用户预置的联系人带过去,和之前项目保持一致。 当然也可以提 case 给 google,咨询是否可以按上面的方案修改,测试前也清空一下子用户的联系人数据。

5. 解决方案

测试逻辑:(统一用户下)根据条件查询联系人数量 预期结果:查询到2个联系人 实际结果: 查询到4个联系人

失败原因:测试过程中会创建子用户,而 google 测试前只删除了主用户的联系人,没有对子用户的联系人进行删除。 解决方案(二选一):

    1. 新增子用户时,不预置联系人
    1. 提case建议google对套件进行修改