1 /* 2 * Copyright (C) 2021 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 package com.android.systemui.flags 17 18 import android.content.BroadcastReceiver 19 import android.content.Context 20 import android.content.Intent 21 import android.content.pm.PackageManager.NameNotFoundException 22 import android.content.res.Resources 23 import android.content.res.Resources.NotFoundException 24 import android.test.suitebuilder.annotation.SmallTest 25 import com.android.systemui.SysuiTestCase 26 import com.android.systemui.util.mockito.any 27 import com.android.systemui.util.mockito.eq 28 import com.android.systemui.util.mockito.nullable 29 import com.android.systemui.util.mockito.withArgCaptor 30 import com.android.systemui.util.settings.GlobalSettings 31 import com.google.common.truth.Truth.assertThat 32 import org.junit.Assert 33 import org.junit.Before 34 import org.junit.Test 35 import org.mockito.ArgumentMatchers.anyInt 36 import org.mockito.Mock 37 import org.mockito.Mockito.anyBoolean 38 import org.mockito.Mockito.anyString 39 import org.mockito.Mockito.inOrder 40 import org.mockito.Mockito.never 41 import org.mockito.Mockito.times 42 import org.mockito.Mockito.verify 43 import org.mockito.Mockito.verifyNoMoreInteractions 44 import org.mockito.MockitoAnnotations 45 import java.io.PrintWriter 46 import java.io.Serializable 47 import java.io.StringWriter 48 import java.util.function.Consumer 49 import org.mockito.Mockito.`when` as whenever 50 51 /** 52 * NOTE: This test is for the version of FeatureFlagManager in src-debug, which allows overriding 53 * the default. 54 */ 55 @SmallTest 56 class FeatureFlagsDebugTest : SysuiTestCase() { 57 private lateinit var featureFlagsDebug: FeatureFlagsDebug 58 59 @Mock 60 private lateinit var flagManager: FlagManager 61 @Mock 62 private lateinit var mockContext: Context 63 @Mock 64 private lateinit var globalSettings: GlobalSettings 65 @Mock 66 private lateinit var systemProperties: SystemPropertiesHelper 67 @Mock 68 private lateinit var resources: Resources 69 @Mock 70 private lateinit var restarter: Restarter 71 private val flagMap = mutableMapOf<String, Flag<*>>() 72 private lateinit var broadcastReceiver: BroadcastReceiver 73 private lateinit var clearCacheAction: Consumer<String> 74 private val serverFlagReader = ServerFlagReaderFake() 75 76 private val teamfoodableFlagA = UnreleasedFlag( 77 name = "a", namespace = "test", teamfood = true 78 ) 79 private val teamfoodableFlagB = ReleasedFlag( 80 name = "b", namespace = "test", teamfood = true 81 ) 82 83 @Before 84 fun setup() { 85 MockitoAnnotations.initMocks(this) 86 flagMap.put(Flags.TEAMFOOD.name, Flags.TEAMFOOD) 87 flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA) 88 flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB) 89 featureFlagsDebug = FeatureFlagsDebug( 90 flagManager, 91 mockContext, 92 globalSettings, 93 systemProperties, 94 resources, 95 serverFlagReader, 96 flagMap, 97 restarter 98 ) 99 featureFlagsDebug.init() 100 verify(flagManager).onSettingsChangedAction = any() 101 broadcastReceiver = withArgCaptor { 102 verify(mockContext).registerReceiver( 103 capture(), any(), nullable(), nullable(), 104 any() 105 ) 106 } 107 clearCacheAction = withArgCaptor { 108 verify(flagManager).clearCacheAction = capture() 109 } 110 whenever(flagManager.nameToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" } 111 } 112 113 @Test 114 fun readBooleanFlag() { 115 // Remember that the TEAMFOOD flag is id#1 and has special behavior. 116 whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true) 117 whenever(flagManager.readFlagValue<Boolean>(eq("4"), any())).thenReturn(false) 118 119 assertThat( 120 featureFlagsDebug.isEnabled( 121 ReleasedFlag( 122 name = "2", 123 namespace = "test" 124 ) 125 ) 126 ).isTrue() 127 assertThat( 128 featureFlagsDebug.isEnabled( 129 UnreleasedFlag( 130 name = "3", 131 namespace = "test" 132 ) 133 ) 134 ).isTrue() 135 assertThat( 136 featureFlagsDebug.isEnabled( 137 ReleasedFlag( 138 name = "4", 139 namespace = "test" 140 ) 141 ) 142 ).isFalse() 143 assertThat( 144 featureFlagsDebug.isEnabled( 145 UnreleasedFlag( 146 name = "5", 147 namespace = "test" 148 ) 149 ) 150 ).isFalse() 151 } 152 153 @Test 154 fun teamFoodFlag_False() { 155 whenever(flagManager.readFlagValue<Boolean>( 156 eq(Flags.TEAMFOOD.name), any())).thenReturn(false) 157 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isFalse() 158 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue() 159 160 // Regular boolean flags should still test the same. 161 // Only our teamfoodableFlag should change. 162 readBooleanFlag() 163 } 164 165 @Test 166 fun teamFoodFlag_True() { 167 whenever(flagManager.readFlagValue<Boolean>( 168 eq(Flags.TEAMFOOD.name), any())).thenReturn(true) 169 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue() 170 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue() 171 172 // Regular boolean flags should still test the same. 173 // Only our teamfoodableFlag should change. 174 readBooleanFlag() 175 } 176 177 @Test 178 fun teamFoodFlag_Overridden() { 179 whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.name), any())) 180 .thenReturn(true) 181 whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any())) 182 .thenReturn(false) 183 whenever(flagManager.readFlagValue<Boolean>( 184 eq(Flags.TEAMFOOD.name), any())).thenReturn(true) 185 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue() 186 assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse() 187 188 // Regular boolean flags should still test the same. 189 // Only our teamfoodableFlag should change. 190 readBooleanFlag() 191 } 192 193 @Test 194 fun readResourceBooleanFlag() { 195 whenever(resources.getBoolean(1001)).thenReturn(false) 196 whenever(resources.getBoolean(1002)).thenReturn(true) 197 whenever(resources.getBoolean(1003)).thenReturn(false) 198 whenever(resources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() } 199 whenever(resources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() } 200 201 whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true) 202 whenever(flagManager.readFlagValue<Boolean>(eq("5"), any())).thenReturn(false) 203 204 assertThat( 205 featureFlagsDebug.isEnabled( 206 ResourceBooleanFlag( 207 "1", 208 "test", 209 1001 210 ) 211 ) 212 ).isFalse() 213 assertThat(featureFlagsDebug.isEnabled(ResourceBooleanFlag("2", "test", 1002))).isTrue() 214 assertThat(featureFlagsDebug.isEnabled(ResourceBooleanFlag("3", "test", 1003))).isTrue() 215 216 Assert.assertThrows(NameNotFoundException::class.java) { 217 featureFlagsDebug.isEnabled(ResourceBooleanFlag("4", "test", 1004)) 218 } 219 // Test that resource is loaded (and validated) even when the setting is set. 220 // This prevents developers from not noticing when they reference an invalid resource. 221 Assert.assertThrows(NameNotFoundException::class.java) { 222 featureFlagsDebug.isEnabled(ResourceBooleanFlag("5", "test", 1005)) 223 } 224 } 225 226 @Test 227 fun readSysPropBooleanFlag() { 228 whenever(systemProperties.getBoolean(anyString(), anyBoolean())).thenAnswer { 229 if ("b".equals(it.getArgument<String?>(0))) { 230 return@thenAnswer true 231 } 232 return@thenAnswer it.getArgument(1) 233 } 234 235 assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("a", "test"))).isFalse() 236 assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("b", "test"))).isTrue() 237 assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("c", "test", true))).isTrue() 238 assertThat( 239 featureFlagsDebug.isEnabled( 240 SysPropBooleanFlag( 241 "d", 242 "test", 243 false 244 ) 245 ) 246 ).isFalse() 247 assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("e", "test"))).isFalse() 248 } 249 250 @Test 251 fun readStringFlag() { 252 whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("foo") 253 whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("bar") 254 assertThat(featureFlagsDebug.getString(StringFlag("1", "test", "biz"))).isEqualTo("biz") 255 assertThat(featureFlagsDebug.getString(StringFlag("2", "test", "baz"))).isEqualTo("baz") 256 assertThat(featureFlagsDebug.getString(StringFlag("3", "test", "buz"))).isEqualTo("foo") 257 assertThat(featureFlagsDebug.getString(StringFlag("4", "test", "buz"))).isEqualTo("bar") 258 } 259 260 @Test 261 fun readResourceStringFlag() { 262 whenever(resources.getString(1001)).thenReturn("") 263 whenever(resources.getString(1002)).thenReturn("resource2") 264 whenever(resources.getString(1003)).thenReturn("resource3") 265 whenever(resources.getString(1004)).thenReturn(null) 266 whenever(resources.getString(1005)).thenAnswer { throw NameNotFoundException() } 267 whenever(resources.getString(1006)).thenAnswer { throw NameNotFoundException() } 268 269 whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("override3") 270 whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("override4") 271 whenever(flagManager.readFlagValue<String>(eq("6"), any())).thenReturn("override6") 272 273 assertThat( 274 featureFlagsDebug.getString( 275 ResourceStringFlag( 276 "1", 277 "test", 278 1001 279 ) 280 ) 281 ).isEqualTo("") 282 assertThat( 283 featureFlagsDebug.getString( 284 ResourceStringFlag( 285 "2", 286 "test", 287 1002 288 ) 289 ) 290 ).isEqualTo("resource2") 291 assertThat( 292 featureFlagsDebug.getString( 293 ResourceStringFlag( 294 "3", 295 "test", 296 1003 297 ) 298 ) 299 ).isEqualTo("override3") 300 301 Assert.assertThrows(NullPointerException::class.java) { 302 featureFlagsDebug.getString(ResourceStringFlag("4", "test", 1004)) 303 } 304 Assert.assertThrows(NameNotFoundException::class.java) { 305 featureFlagsDebug.getString(ResourceStringFlag("5", "test", 1005)) 306 } 307 // Test that resource is loaded (and validated) even when the setting is set. 308 // This prevents developers from not noticing when they reference an invalid resource. 309 Assert.assertThrows(NameNotFoundException::class.java) { 310 featureFlagsDebug.getString(ResourceStringFlag("6", "test", 1005)) 311 } 312 } 313 314 @Test 315 fun readIntFlag() { 316 whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(22) 317 whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(48) 318 assertThat(featureFlagsDebug.getInt(IntFlag("1", "test", 12))).isEqualTo(12) 319 assertThat(featureFlagsDebug.getInt(IntFlag("2", "test", 93))).isEqualTo(93) 320 assertThat(featureFlagsDebug.getInt(IntFlag("3", "test", 8))).isEqualTo(22) 321 assertThat(featureFlagsDebug.getInt(IntFlag("4", "test", 234))).isEqualTo(48) 322 } 323 324 @Test 325 fun readResourceIntFlag() { 326 whenever(resources.getInteger(1001)).thenReturn(88) 327 whenever(resources.getInteger(1002)).thenReturn(61) 328 whenever(resources.getInteger(1003)).thenReturn(9342) 329 whenever(resources.getInteger(1004)).thenThrow(NotFoundException("unknown resource")) 330 whenever(resources.getInteger(1005)).thenThrow(NotFoundException("unknown resource")) 331 whenever(resources.getInteger(1006)).thenThrow(NotFoundException("unknown resource")) 332 333 whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(20) 334 whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(500) 335 whenever(flagManager.readFlagValue<Int>(eq("5"), any())).thenReturn(9519) 336 337 assertThat(featureFlagsDebug.getInt(ResourceIntFlag("1", "test", 1001))).isEqualTo(88) 338 assertThat(featureFlagsDebug.getInt(ResourceIntFlag("2", "test", 1002))).isEqualTo(61) 339 assertThat(featureFlagsDebug.getInt(ResourceIntFlag("3", "test", 1003))).isEqualTo(20) 340 341 Assert.assertThrows(NotFoundException::class.java) { 342 featureFlagsDebug.getInt(ResourceIntFlag("4", "test", 1004)) 343 } 344 // Test that resource is loaded (and validated) even when the setting is set. 345 // This prevents developers from not noticing when they reference an invalid resource. 346 Assert.assertThrows(NotFoundException::class.java) { 347 featureFlagsDebug.getInt(ResourceIntFlag("5", "test", 1005)) 348 } 349 } 350 351 @Test 352 fun broadcastReceiver_IgnoresInvalidData() { 353 addFlag(UnreleasedFlag("1", "test")) 354 addFlag(ResourceBooleanFlag("2", "test", 1002)) 355 addFlag(StringFlag("3", "test", "flag3")) 356 addFlag(ResourceStringFlag("4", "test", 1004)) 357 358 broadcastReceiver.onReceive(mockContext, null) 359 broadcastReceiver.onReceive(mockContext, Intent()) 360 broadcastReceiver.onReceive(mockContext, Intent("invalid action")) 361 broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG)) 362 setByBroadcast("0", false) // unknown id does nothing 363 setByBroadcast("1", "string") // wrong type does nothing 364 setByBroadcast("2", 123) // wrong type does nothing 365 setByBroadcast("3", false) // wrong type does nothing 366 setByBroadcast("4", 123) // wrong type does nothing 367 verifyNoMoreInteractions(flagManager, globalSettings) 368 } 369 370 @Test 371 fun intentWithId_NoValueKeyClears() { 372 addFlag(UnreleasedFlag(name = "1", namespace = "test")) 373 374 // trying to erase an id not in the map does nothing 375 broadcastReceiver.onReceive( 376 mockContext, 377 Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "") 378 ) 379 verifyNoMoreInteractions(flagManager, globalSettings) 380 381 // valid id with no value puts empty string in the setting 382 broadcastReceiver.onReceive( 383 mockContext, 384 Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "1") 385 ) 386 verifyPutData("1", "", numReads = 0) 387 } 388 389 @Test 390 fun setBooleanFlag() { 391 addFlag(UnreleasedFlag("1", "test")) 392 addFlag(UnreleasedFlag("2", "test")) 393 addFlag(ResourceBooleanFlag("3", "test", 1003)) 394 addFlag(ResourceBooleanFlag("4", "test", 1004)) 395 396 setByBroadcast("1", false) 397 verifyPutData("1", "{\"type\":\"boolean\",\"value\":false}") 398 399 setByBroadcast("2", true) 400 verifyPutData("2", "{\"type\":\"boolean\",\"value\":true}") 401 402 setByBroadcast("3", false) 403 verifyPutData("3", "{\"type\":\"boolean\",\"value\":false}") 404 405 setByBroadcast("4", true) 406 verifyPutData("4", "{\"type\":\"boolean\",\"value\":true}") 407 } 408 409 @Test 410 fun setStringFlag() { 411 addFlag(StringFlag("1", "1", "test")) 412 addFlag(ResourceStringFlag("2", "test", 1002)) 413 414 setByBroadcast("1", "override1") 415 verifyPutData("1", "{\"type\":\"string\",\"value\":\"override1\"}") 416 417 setByBroadcast("2", "override2") 418 verifyPutData("2", "{\"type\":\"string\",\"value\":\"override2\"}") 419 } 420 421 @Test 422 fun setFlag_ClearsCache() { 423 val flag1 = addFlag(StringFlag("1", "test", "flag1")) 424 whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("original") 425 426 // gets the flag & cache it 427 assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("original") 428 verify(flagManager, times(1)).readFlagValue(eq("1"), eq(StringFlagSerializer)) 429 430 // hit the cache 431 assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("original") 432 verifyNoMoreInteractions(flagManager) 433 434 // set the flag 435 setByBroadcast("1", "new") 436 verifyPutData("1", "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2) 437 whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("new") 438 439 assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("new") 440 verify(flagManager, times(3)).readFlagValue(eq("1"), eq(StringFlagSerializer)) 441 } 442 443 @Test 444 fun serverSide_Overrides_MakesFalse() { 445 val flag = ReleasedFlag("100", "test") 446 447 serverFlagReader.setFlagValue(flag.namespace, flag.name, false) 448 449 assertThat(featureFlagsDebug.isEnabled(flag)).isFalse() 450 } 451 452 @Test 453 fun serverSide_Overrides_MakesTrue() { 454 val flag = UnreleasedFlag(name = "100", namespace = "test") 455 456 serverFlagReader.setFlagValue(flag.namespace, flag.name, true) 457 assertThat(featureFlagsDebug.isEnabled(flag)).isTrue() 458 } 459 460 @Test 461 fun serverSide_OverrideUncached_NoRestart() { 462 // No one has read the flag, so it's not in the cache. 463 serverFlagReader.setFlagValue( 464 teamfoodableFlagA.namespace, teamfoodableFlagA.name, !teamfoodableFlagA.default) 465 verify(restarter, never()).restartSystemUI(anyString()) 466 } 467 468 @Test 469 fun serverSide_Override_Restarts() { 470 // Read it to put it in the cache. 471 featureFlagsDebug.isEnabled(teamfoodableFlagA) 472 serverFlagReader.setFlagValue( 473 teamfoodableFlagA.namespace, teamfoodableFlagA.name, !teamfoodableFlagA.default) 474 verify(restarter).restartSystemUI(anyString()) 475 } 476 477 @Test 478 fun serverSide_RedundantOverride_NoRestart() { 479 // Read it to put it in the cache. 480 featureFlagsDebug.isEnabled(teamfoodableFlagA) 481 serverFlagReader.setFlagValue( 482 teamfoodableFlagA.namespace, teamfoodableFlagA.name, teamfoodableFlagA.default) 483 verify(restarter, never()).restartSystemUI(anyString()) 484 } 485 486 @Test 487 fun dumpFormat() { 488 val flag1 = ReleasedFlag("1", "test") 489 val flag2 = ResourceBooleanFlag("2", "test", 1002) 490 val flag3 = UnreleasedFlag("3", "test") 491 val flag4 = StringFlag("4", "test", "") 492 val flag5 = StringFlag("5", "test", "flag5default") 493 val flag6 = ResourceStringFlag("6", "test", 1006) 494 val flag7 = ResourceStringFlag("7", "test", 1007) 495 496 whenever(resources.getBoolean(1002)).thenReturn(true) 497 whenever(resources.getString(1006)).thenReturn("resource1006") 498 whenever(resources.getString(1007)).thenReturn("resource1007") 499 whenever(flagManager.readFlagValue(eq("7"), eq(StringFlagSerializer))) 500 .thenReturn("override7") 501 502 // WHEN the flags have been accessed 503 assertThat(featureFlagsDebug.isEnabled(flag1)).isTrue() 504 assertThat(featureFlagsDebug.isEnabled(flag2)).isTrue() 505 assertThat(featureFlagsDebug.isEnabled(flag3)).isFalse() 506 assertThat(featureFlagsDebug.getString(flag4)).isEmpty() 507 assertThat(featureFlagsDebug.getString(flag5)).isEqualTo("flag5default") 508 assertThat(featureFlagsDebug.getString(flag6)).isEqualTo("resource1006") 509 assertThat(featureFlagsDebug.getString(flag7)).isEqualTo("override7") 510 511 // THEN the dump contains the flags and the default values 512 val dump = dumpToString() 513 assertThat(dump).contains(" sysui_flag_1: true\n") 514 assertThat(dump).contains(" sysui_flag_2: true\n") 515 assertThat(dump).contains(" sysui_flag_3: false\n") 516 assertThat(dump).contains(" sysui_flag_4: [length=0] \"\"\n") 517 assertThat(dump).contains(" sysui_flag_5: [length=12] \"flag5default\"\n") 518 assertThat(dump).contains(" sysui_flag_6: [length=12] \"resource1006\"\n") 519 assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n") 520 } 521 522 private fun verifyPutData(name: String, data: String, numReads: Int = 1) { 523 inOrder(flagManager, globalSettings).apply { 524 verify(flagManager, times(numReads)).readFlagValue(eq(name), any<FlagSerializer<*>>()) 525 verify(flagManager).nameToSettingsKey(eq(name)) 526 verify(globalSettings).putStringForUser(eq("key-$name"), eq(data), anyInt()) 527 verify(flagManager).dispatchListenersAndMaybeRestart(eq(name), any()) 528 }.verifyNoMoreInteractions() 529 verifyNoMoreInteractions(flagManager, globalSettings) 530 } 531 532 private fun setByBroadcast(name: String, value: Serializable?) { 533 val intent = Intent(FlagManager.ACTION_SET_FLAG) 534 intent.putExtra(FlagManager.EXTRA_NAME, name) 535 intent.putExtra(FlagManager.EXTRA_VALUE, value) 536 broadcastReceiver.onReceive(mockContext, intent) 537 } 538 539 private fun <F : Flag<*>> addFlag(flag: F): F { 540 val old = flagMap.put(flag.name, flag) 541 check(old == null) { "Flag ${flag.name} already registered" } 542 return flag 543 } 544 545 private fun dumpToString(): String { 546 val sw = StringWriter() 547 val pw = PrintWriter(sw) 548 featureFlagsDebug.dump(pw, emptyArray<String>()) 549 pw.flush() 550 return sw.toString() 551 } 552 } 553