blob: b43856aae4cf78684e8d4d1b8bd0e684a04a4f01 [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.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 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.MockitoAnnotations
import java.io.PrintWriter
import java.io.Serializable
import java.io.StringWriter
import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
/**
* 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 teamfoodableFlagA = BooleanFlag(500, false, true)
private val teamfoodableFlagB = BooleanFlag(501, true, 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,
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 testReadBooleanFlag() {
// 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(BooleanFlag(2, true))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(3, false))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(4, true))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(5, false))).isFalse()
}
@Test
fun testTeamFoodFlag_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.
testReadBooleanFlag()
}
@Test
fun testTeamFoodFlag_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.
testReadBooleanFlag()
}
@Test
fun testTeamFoodFlag_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.
testReadBooleanFlag()
}
@Test
fun testReadResourceBooleanFlag() {
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 testReadSysPropBooleanFlag() {
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 testReadStringFlag() {
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 testReadResourceStringFlag() {
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 testBroadcastReceiverIgnoresInvalidData() {
addFlag(BooleanFlag(1, false))
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 testIntentWithIdButNoValueKeyClears() {
addFlag(BooleanFlag(1, false))
// trying to erase an id not in the map does noting
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 testSetBooleanFlag() {
addFlag(BooleanFlag(1, false))
addFlag(BooleanFlag(2, false))
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 testSetStringFlag() {
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 testSetFlagClearsCache() {
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 testRegisterCommand() {
verify(commandRegistry).registerCommand(anyString(), any())
}
@Test
fun testNoOpCommand() {
val cmd = captureCommand()
cmd.execute(pw, ArrayList())
verify(pw, atLeastOnce()).println()
verify(flagManager).readFlagValue<Boolean>(eq(1), any())
verifyZeroInteractions(secureSettings)
}
@Test
fun testReadFlagCommand() {
addFlag(BooleanFlag(1, false))
val cmd = captureCommand()
cmd.execute(pw, listOf("1"))
verify(flagManager).readFlagValue<Boolean>(eq(1), any())
}
@Test
fun testSetFlagCommand() {
addFlag(BooleanFlag(1, false))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "on"))
verifyPutData(1, "{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun testToggleFlagCommand() {
addFlag(BooleanFlag(1, true))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "toggle"))
verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}", 2)
}
@Test
fun testEraseFlagCommand() {
addFlag(BooleanFlag(1, true))
val cmd = captureCommand()
cmd.execute(pw, listOf("1", "erase"))
verify(secureSettings).putStringForUser(eq("key-1"), eq(""), anyInt())
}
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()
}
@Test
fun testDump() {
val flag1 = BooleanFlag(1, true)
val flag2 = ResourceBooleanFlag(2, 1002)
val flag3 = BooleanFlag(3, false)
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 dumpToString(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
mFeatureFlagsDebug.dump(pw, emptyArray<String>())
pw.flush()
return sw.toString()
}
}