1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm.test 18 19 import com.android.internal.util.test.SystemPreparer 20 import com.android.tradefed.device.ITestDevice 21 import com.google.common.truth.Truth 22 import org.junit.rules.TemporaryFolder 23 import java.io.File 24 import java.io.FileOutputStream 25 26 internal fun SystemPreparer.pushApk(javaResourceName: String, partition: Partition) = 27 pushResourceFile(javaResourceName, HostUtils.makePathForApk(javaResourceName, partition) 28 .toString()) 29 30 internal fun SystemPreparer.deleteApkFolders( 31 partition: Partition, 32 vararg javaResourceNames: String 33 ) = apply { 34 javaResourceNames.forEach { 35 deleteFile(partition.baseAppFolder.resolve(it.removeSuffix(".apk")).toString()) 36 } 37 } 38 39 internal fun ITestDevice.installJavaResourceApk( 40 tempFolder: TemporaryFolder, 41 javaResource: String, 42 reinstall: Boolean = true, 43 extraArgs: Array<String> = emptyArray() 44 ): String? { 45 val file = HostUtils.copyResourceToHostFile(javaResource, tempFolder.newFile()) 46 return installPackage(file, reinstall, *extraArgs) 47 } 48 49 internal fun ITestDevice.uninstallPackages(vararg pkgNames: String) = 50 pkgNames.forEach { uninstallPackage(it) } 51 52 /** 53 * Retry [block] a total of [maxAttempts] times, waiting [millisBetweenAttempts] milliseconds 54 * between each iteration, until a non-null result is returned, providing that result back to the 55 * caller. 56 * 57 * If an [AssertionError] is thrown by the [block] and a non-null result is never returned, that 58 * error will be re-thrown. This allows the use of [Truth.assertThat] to indicate success while 59 * providing a meaningful error message in case of failure. 60 */ 61 internal fun <T> retryUntilNonNull( 62 maxAttempts: Int = 10, 63 millisBetweenAttempts: Long = 1000, 64 block: () -> T? 65 ): T { 66 var attempt = 0 67 var failure: AssertionError? = null 68 while (attempt++ < maxAttempts) { 69 val result = try { 70 block() 71 } catch (e: AssertionError) { 72 failure = e 73 null 74 } 75 76 if (result != null) { 77 return result 78 } else { 79 Thread.sleep(millisBetweenAttempts) 80 } 81 } 82 83 throw failure ?: AssertionError("Never succeeded") 84 } 85 86 internal fun retryUntilSuccess(block: () -> Boolean) { 87 retryUntilNonNull { block().takeIf { it } } 88 } 89 90 internal object HostUtils { 91 92 fun getDataDir(device: ITestDevice, pkgName: String) = 93 device.executeShellCommand("dumpsys package $pkgName") 94 .lineSequence() 95 .map(String::trim) 96 .single { it.startsWith("dataDir=") } 97 .removePrefix("dataDir=") 98 99 fun makePathForApk(fileName: String, partition: Partition) = 100 makePathForApk(File(fileName), partition) 101 102 fun makePathForApk(file: File, partition: Partition) = 103 partition.baseAppFolder 104 .resolve(file.nameWithoutExtension) 105 .resolve(file.name) 106 107 fun copyResourceToHostFile(javaResourceName: String, file: File): File { 108 javaClass.classLoader!!.getResource(javaResourceName).openStream().use { input -> 109 FileOutputStream(file).use { output -> 110 input.copyTo(output) 111 } 112 } 113 return file 114 } 115 116 /** 117 * dumpsys package and therefore device.getAppPackageInfo doesn't work immediately after reboot, 118 * so the following methods parse the package dump directly to see if the path matches. 119 */ 120 121 /** 122 * Reads the pm dump for a package name starting from the Packages: metadata section until 123 * the following section. 124 */ 125 fun packageSection( 126 device: ITestDevice, 127 pkgName: String, 128 sectionName: String = "Packages" 129 ) = device.executeShellCommand("pm dump $pkgName") 130 .lineSequence() 131 .dropWhile { !it.startsWith(sectionName) } // Wait until the header 132 .drop(1) // Drop the header itself 133 .takeWhile { 134 // Until next top level header, a non-empty line that doesn't start with whitespace 135 it.isEmpty() || it.first().isWhitespace() 136 } 137 .map(String::trim) 138 139 fun getCodePaths(device: ITestDevice, pkgName: String) = 140 device.executeShellCommand("pm dump $pkgName") 141 .lineSequence() 142 .map(String::trim) 143 .filter { it.startsWith("codePath=") } 144 .map { it.removePrefix("codePath=") } 145 .toList() 146 147 private fun userIdLineSequence(device: ITestDevice, pkgName: String) = 148 packageSection(device, pkgName) 149 .filter { it.startsWith("User ") } 150 151 fun getUserIdToPkgEnabledState(device: ITestDevice, pkgName: String) = 152 userIdLineSequence(device, pkgName).associate { 153 val userId = it.removePrefix("User ") 154 .takeWhile(Char::isDigit) 155 .toInt() 156 val enabled = it.substringAfter("enabled=") 157 .takeWhile(Char::isDigit) 158 .toInt() 159 .let { 160 when (it) { 161 0, 1 -> true 162 else -> false 163 } 164 } 165 userId to enabled 166 } 167 168 fun getUserIdToPkgInstalledState(device: ITestDevice, pkgName: String) = 169 userIdLineSequence(device, pkgName).associate { 170 val userId = it.removePrefix("User ") 171 .takeWhile(Char::isDigit) 172 .toInt() 173 val installed = it.substringAfter("installed=") 174 .takeWhile { !it.isWhitespace() } 175 .toBoolean() 176 userId to installed 177 } 178 } 179