blob: 4511193d41d3add5ab1ccd04124579da33e091dd [file] [log] [blame]
/*
* Copyright (C) 2021 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.systemui.flags
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.content.res.Resources
import android.test.suitebuilder.annotation.SmallTest
import com.android.internal.statusbar.IStatusBarService
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.Serializable
import java.io.StringWriter
import java.util.function.Consumer
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyString
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
/**
* NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow
* overriding, and should never return any value other than the one provided as the default.
*/
@SmallTest
class FeatureFlagsDebugTest : SysuiTestCase() {
private lateinit var mFeatureFlagsDebug: FeatureFlagsDebug
@Mock private lateinit var flagManager: FlagManager
@Mock private lateinit var mockContext: Context
@Mock private lateinit var secureSettings: SecureSettings
@Mock private lateinit var systemProperties: SystemPropertiesHelper
@Mock private lateinit var resources: Resources
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var commandRegistry: CommandRegistry
@Mock private lateinit var barService: IStatusBarService
@Mock private lateinit var pw: PrintWriter
private val flagMap = mutableMapOf<Int, Flag<*>>()
private lateinit var broadcastReceiver: BroadcastReceiver
private lateinit var clearCacheAction: Consumer<Int>
private val serverFlagReader = ServerFlagReaderFake()
private val deviceConfig = DeviceConfigProxyFake()
private val teamfoodableFlagA = UnreleasedFlag(500, true)
private val teamfoodableFlagB = ReleasedFlag(501, true)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
flagMap.put(teamfoodableFlagA.id, teamfoodableFlagA)
flagMap.put(teamfoodableFlagB.id, teamfoodableFlagB)
mFeatureFlagsDebug = FeatureFlagsDebug(
flagManager,
mockContext,
secureSettings,
systemProperties,
resources,
dumpManager,
deviceConfig,
serverFlagReader,
flagMap,
commandRegistry,
barService
)
verify(flagManager).onSettingsChangedAction = any()
broadcastReceiver = withArgCaptor {
verify(mockContext).registerReceiver(capture(), any(), nullable(), nullable(),
any())
}
clearCacheAction = withArgCaptor {
verify(flagManager).clearCacheAction = capture()
}
whenever(flagManager.idToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
}
@Test
fun readBooleanFlag() {
// Remember that the TEAMFOOD flag is id#1 and has special behavior.
whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
whenever(flagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(2))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(3))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(4))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(5))).isFalse()
}
@Test
fun teamFoodFlag_False() {
whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
// Regular boolean flags should still test the same.
// Only our teamfoodableFlag should change.
readBooleanFlag()
}
@Test
fun teamFoodFlag_True() {
whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
// Regular boolean flags should still test the same.
// Only our teamfoodableFlag should change.
readBooleanFlag()
}
@Test
fun teamFoodFlag_Overridden() {
whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.id), any()))
.thenReturn(true)
whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.id), any()))
.thenReturn(false)
whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
// Regular boolean flags should still test the same.
// Only our teamfoodableFlag should change.
readBooleanFlag()
}
@Test
fun readResourceBooleanFlag() {
whenever(resources.getBoolean(1001)).thenReturn(false)
whenever(resources.getBoolean(1002)).thenReturn(true)
whenever(resources.getBoolean(1003)).thenReturn(false)
whenever(resources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() }
whenever(resources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() }
whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
whenever(flagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(1, 1001))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, 1002))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, 1003))).isTrue()
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, 1004))
}
// Test that resource is loaded (and validated) even when the setting is set.
// This prevents developers from not noticing when they reference an invalid resource.
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, 1005))
}
}
@Test
fun readSysPropBooleanFlag() {
whenever(systemProperties.getBoolean(anyString(), anyBoolean())).thenAnswer {
if ("b".equals(it.getArgument<String?>(0))) {
return@thenAnswer true
}
return@thenAnswer it.getArgument(1)
}
assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(1, "a"))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(2, "b"))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(3, "c", true))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(4, "d", false))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(5, "e"))).isFalse()
}
@Test
fun readDeviceConfigBooleanFlag() {
val namespace = "test_namespace"
deviceConfig.setProperty(namespace, "a", "true", false)
deviceConfig.setProperty(namespace, "b", "false", false)
deviceConfig.setProperty(namespace, "c", null, false)
assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(1, "a", namespace)))
.isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(2, "b", namespace)))
.isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(3, "c", namespace)))
.isFalse()
}
@Test
fun readStringFlag() {
whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "biz"))).isEqualTo("biz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "baz"))).isEqualTo("baz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "buz"))).isEqualTo("foo")
assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "buz"))).isEqualTo("bar")
}
@Test
fun readResourceStringFlag() {
whenever(resources.getString(1001)).thenReturn("")
whenever(resources.getString(1002)).thenReturn("resource2")
whenever(resources.getString(1003)).thenReturn("resource3")
whenever(resources.getString(1004)).thenReturn(null)
whenever(resources.getString(1005)).thenAnswer { throw NameNotFoundException() }
whenever(resources.getString(1006)).thenAnswer { throw NameNotFoundException() }
whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("override3")
whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
whenever(flagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(2, 1002))).isEqualTo("resource2")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(3, 1003))).isEqualTo("override3")
Assert.assertThrows(NullPointerException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(4, 1004))
}
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(5, 1005))
}
// Test that resource is loaded (and validated) even when the setting is set.
// This prevents developers from not noticing when they reference an invalid resource.
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(6, 1005))
}
}
@Test
fun broadcastReceiver_IgnoresInvalidData() {
addFlag(UnreleasedFlag(1))
addFlag(ResourceBooleanFlag(2, 1002))
addFlag(StringFlag(3, "flag3"))
addFlag(ResourceStringFlag(4, 1004))
broadcastReceiver.onReceive(mockContext, null)
broadcastReceiver.onReceive(mockContext, Intent())
broadcastReceiver.onReceive(mockContext, Intent("invalid action"))
broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG))
setByBroadcast(0, false) // unknown id does nothing
setByBroadcast(1, "string") // wrong type does nothing
setByBroadcast(2, 123) // wrong type does nothing
setByBroadcast(3, false) // wrong type does nothing
setByBroadcast(4, 123) // wrong type does nothing
verifyNoMoreInteractions(flagManager, secureSettings)
}
@Test
fun intentWithId_NoValueKeyClears() {
addFlag(UnreleasedFlag(1))
// trying to erase an id not in the map does nothing
broadcastReceiver.onReceive(
mockContext,
Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 0)
)
verifyNoMoreInteractions(flagManager, secureSettings)
// valid id with no value puts empty string in the setting
broadcastReceiver.onReceive(
mockContext,
Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 1)
)
verifyPutData(1, "", numReads = 0)
}
@Test
fun setBooleanFlag() {
addFlag(UnreleasedFlag(1))
addFlag(UnreleasedFlag(2))
addFlag(ResourceBooleanFlag(3, 1003))
addFlag(ResourceBooleanFlag(4, 1004))
setByBroadcast(1, false)
verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
setByBroadcast(2, true)
verifyPutData(2, "{\"type\":\"boolean\",\"value\":true}")
setByBroadcast(3, false)
verifyPutData(3, "{\"type\":\"boolean\",\"value\":false}")
setByBroadcast(4, true)
verifyPutData(4, "{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun setStringFlag() {
addFlag(StringFlag(1, "flag1"))
addFlag(ResourceStringFlag(2, 1002))
setByBroadcast(1, "override1")
verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
setByBroadcast(2, "override2")
verifyPutData(2, "{\"type\":\"string\",\"value\":\"override2\"}")
}
@Test
fun setFlag_ClearsCache() {
val flag1 = addFlag(StringFlag(1, "flag1"))
whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
// gets the flag & cache it
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
verify(flagManager).readFlagValue(eq(1), eq(StringFlagSerializer))
// hit the cache
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
verifyNoMoreInteractions(flagManager)
// set the flag
setByBroadcast(1, "new")
verifyPutData(1, "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("new")
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("new")
verify(flagManager, times(3)).readFlagValue(eq(1), eq(StringFlagSerializer))
}
@Test
fun serverSide_Overrides_MakesFalse() {
val flag = ReleasedFlag(100)
serverFlagReader.setFlagValue(flag.id, false)
assertThat(mFeatureFlagsDebug.isEnabled(flag)).isFalse()
}
@Test
fun serverSide_Overrides_MakesTrue() {
val flag = UnreleasedFlag(100)
serverFlagReader.setFlagValue(flag.id, true)
assertThat(mFeatureFlagsDebug.isEnabled(flag)).isTrue()
}
@Test
fun statusBarCommand_IsRegistered() {
verify(commandRegistry).registerCommand(anyString(), any())
}
@Test
fun noOpCommand() {
val cmd = captureCommand()
cmd.execute(pw, ArrayList())
verify(pw, atLeastOnce()).println()
verify(flagManager).readFlagValue<Boolean>(eq(1), any())
verifyZeroInteractions(secureSettings)
}
@Test
fun readFlagCommand() {
addFlag(UnreleasedFlag(1))
val cmd = captureCommand()
cmd.execute(pw, listOf("1"))
verify(flagManager).readFlagValue<Boolean>(eq(1), any())
}
@Test
fun setFlagCommand() {
addFlag(UnreleasedFlag(1))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "on"))
verifyPutData(1, "{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun toggleFlagCommand() {
addFlag(ReleasedFlag(1))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "toggle"))
verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}", 2)
}
@Test
fun eraseFlagCommand() {
addFlag(ReleasedFlag(1))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "erase"))
verify(secureSettings).putStringForUser(eq("key-1"), eq(""), anyInt())
}
@Test
fun dumpFormat() {
val flag1 = ReleasedFlag(1)
val flag2 = ResourceBooleanFlag(2, 1002)
val flag3 = UnreleasedFlag(3)
val flag4 = StringFlag(4, "")
val flag5 = StringFlag(5, "flag5default")
val flag6 = ResourceStringFlag(6, 1006)
val flag7 = ResourceStringFlag(7, 1007)
whenever(resources.getBoolean(1002)).thenReturn(true)
whenever(resources.getString(1006)).thenReturn("resource1006")
whenever(resources.getString(1007)).thenReturn("resource1007")
whenever(flagManager.readFlagValue(eq(7), eq(StringFlagSerializer)))
.thenReturn("override7")
// WHEN the flags have been accessed
assertThat(mFeatureFlagsDebug.isEnabled(flag1)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(flag2)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(flag3)).isFalse()
assertThat(mFeatureFlagsDebug.getString(flag4)).isEmpty()
assertThat(mFeatureFlagsDebug.getString(flag5)).isEqualTo("flag5default")
assertThat(mFeatureFlagsDebug.getString(flag6)).isEqualTo("resource1006")
assertThat(mFeatureFlagsDebug.getString(flag7)).isEqualTo("override7")
// THEN the dump contains the flags and the default values
val dump = dumpToString()
assertThat(dump).contains(" sysui_flag_1: true\n")
assertThat(dump).contains(" sysui_flag_2: true\n")
assertThat(dump).contains(" sysui_flag_3: false\n")
assertThat(dump).contains(" sysui_flag_4: [length=0] \"\"\n")
assertThat(dump).contains(" sysui_flag_5: [length=12] \"flag5default\"\n")
assertThat(dump).contains(" sysui_flag_6: [length=12] \"resource1006\"\n")
assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n")
}
private fun verifyPutData(id: Int, data: String, numReads: Int = 1) {
inOrder(flagManager, secureSettings).apply {
verify(flagManager, times(numReads)).readFlagValue(eq(id), any<FlagSerializer<*>>())
verify(flagManager).idToSettingsKey(eq(id))
verify(secureSettings).putStringForUser(eq("key-$id"), eq(data), anyInt())
verify(flagManager).dispatchListenersAndMaybeRestart(eq(id), any())
}.verifyNoMoreInteractions()
verifyNoMoreInteractions(flagManager, secureSettings)
}
private fun setByBroadcast(id: Int, value: Serializable?) {
val intent = Intent(FlagManager.ACTION_SET_FLAG)
intent.putExtra(FlagManager.EXTRA_ID, id)
intent.putExtra(FlagManager.EXTRA_VALUE, value)
broadcastReceiver.onReceive(mockContext, intent)
}
private fun <F : Flag<*>> addFlag(flag: F): F {
val old = flagMap.put(flag.id, flag)
check(old == null) { "Flag ${flag.id} already registered" }
return flag
}
private fun captureCommand(): Command {
val captor = argumentCaptor<Function0<Command>>()
verify(commandRegistry).registerCommand(anyString(), capture(captor))
return captor.value.invoke()
}
private fun dumpToString(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
mFeatureFlagsDebug.dump(pw, emptyArray<String>())
pw.flush()
return sw.toString()
}
}