1 /* <lambda>null2 * Copyright (C) 2022 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.testutils 18 19 import android.Manifest.permission.READ_DEVICE_CONFIG 20 import android.Manifest.permission.WRITE_DEVICE_CONFIG 21 import android.provider.DeviceConfig 22 import android.util.Log 23 import com.android.modules.utils.build.SdkLevel 24 import com.android.testutils.FunctionalUtils.ThrowingRunnable 25 import java.util.concurrent.CompletableFuture 26 import java.util.concurrent.Executor 27 import java.util.concurrent.TimeUnit 28 import org.junit.rules.TestRule 29 import org.junit.runner.Description 30 import org.junit.runners.model.Statement 31 32 private val TAG = DeviceConfigRule::class.simpleName 33 34 private const val TIMEOUT_MS = 20_000L 35 36 /** 37 * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration 38 * automatically on teardown. 39 * 40 * The rule can also optionally retry tests when they fail following an external change of 41 * DeviceConfig before S; this typically happens because device config flags are synced while the 42 * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S. 43 * 44 * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if 45 * the configs that were set through this rule were changed, and retry the test 46 * up to the specified number of times if yes. 47 */ 48 class DeviceConfigRule @JvmOverloads constructor( 49 val retryCountBeforeSIfConfigChanged: Int = 0 50 ) : TestRule { 51 // Maps (namespace, key) -> value 52 private val originalConfig = mutableMapOf<Pair<String, String>, String?>() 53 private val usedConfig = mutableMapOf<Pair<String, String>, String?>() 54 55 /** 56 * Actions to be run after cleanup of the config, for the current test only. 57 */ 58 private val currentTestCleanupActions = mutableListOf<ThrowingRunnable>() 59 60 override fun apply(base: Statement, description: Description): Statement { 61 return TestValidationUrlStatement(base, description) 62 } 63 64 private inner class TestValidationUrlStatement( 65 private val base: Statement, 66 private val description: Description 67 ) : Statement() { 68 override fun evaluate() { 69 var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1 70 while (retryCount > 0) { 71 retryCount-- 72 tryTest { 73 base.evaluate() 74 // Can't use break/return out of a loop here because this is a tryTest lambda, 75 // so set retryCount to exit instead 76 retryCount = 0 77 }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception 78 if (retryCount == 0) throw e 79 usedConfig.forEach { (key, value) -> 80 val currentValue = runAsShell(READ_DEVICE_CONFIG) { 81 DeviceConfig.getProperty(key.first, key.second) 82 } 83 if (currentValue != value) { 84 Log.w(TAG, "Test failed with unexpected device config change, retrying") 85 return@catch 86 } 87 } 88 throw e 89 } cleanupStep { 90 runAsShell(WRITE_DEVICE_CONFIG) { 91 originalConfig.forEach { (key, value) -> 92 DeviceConfig.setProperty( 93 key.first, key.second, value, false /* makeDefault */) 94 } 95 } 96 } cleanupStep { 97 originalConfig.clear() 98 usedConfig.clear() 99 } cleanup { 100 // Fold all cleanup actions into cleanup steps of an empty tryTest, so they are 101 // all run even if exceptions are thrown, and exceptions are reported properly. 102 currentTestCleanupActions.fold(tryTest { }) { 103 tryBlock, action -> tryBlock.cleanupStep { action.run() } 104 }.cleanup { 105 currentTestCleanupActions.clear() 106 } 107 } 108 } 109 } 110 } 111 112 /** 113 * Set a configuration key/value. After the test case ends, it will be restored to the value it 114 * had when this method was first called. 115 */ 116 fun setConfig(namespace: String, key: String, value: String?): String? { 117 Log.i(TAG, "Setting config \"$key\" to \"$value\"") 118 val readWritePermissions = arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) 119 120 val keyPair = Pair(namespace, key) 121 val existingValue = runAsShell(*readWritePermissions) { 122 DeviceConfig.getProperty(namespace, key) 123 } 124 if (!originalConfig.containsKey(keyPair)) { 125 originalConfig[keyPair] = existingValue 126 } 127 usedConfig[keyPair] = value 128 if (existingValue == value) { 129 // Already the correct value. There may be a race if a change is already in flight, 130 // but if multiple threads update the config there is no way to fix that anyway. 131 Log.i(TAG, "\"$key\" already had value \"$value\"") 132 return value 133 } 134 135 val future = CompletableFuture<String>() 136 val listener = DeviceConfig.OnPropertiesChangedListener { 137 // The listener receives updates for any change to any key, so don't react to 138 // changes that do not affect the relevant key 139 if (!it.keyset.contains(key)) return@OnPropertiesChangedListener 140 // "null" means absent in DeviceConfig : there is no such thing as a present but 141 // null value, so the following works even if |value| is null. 142 if (it.getString(key, null) == value) { 143 future.complete(value) 144 } 145 } 146 147 return tryTest { 148 runAsShell(*readWritePermissions) { 149 DeviceConfig.addOnPropertiesChangedListener( 150 namespace, 151 inlineExecutor, 152 listener) 153 DeviceConfig.setProperty( 154 namespace, 155 key, 156 value, 157 false /* makeDefault */) 158 // Don't drop the permission until the config is applied, just in case 159 future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) 160 }.also { 161 Log.i(TAG, "Config \"$key\" successfully set to \"$value\"") 162 } 163 } cleanup { 164 DeviceConfig.removeOnPropertiesChangedListener(listener) 165 } 166 } 167 168 private val inlineExecutor get() = Executor { r -> r.run() } 169 170 /** 171 * Add an action to be run after config cleanup when the current test case ends. 172 */ 173 fun runAfterNextCleanup(action: ThrowingRunnable) { 174 currentTestCleanupActions.add(action) 175 } 176 } 177