Merge "Export PRODUCT_PRODUCT_LINKER_CONFIG_FRAGMENTS to soong" into main
diff --git a/target/product/base_system.mk b/target/product/base_system.mk
index 5ce21e2..eb4d497 100644
--- a/target/product/base_system.mk
+++ b/target/product/base_system.mk
@@ -424,6 +424,7 @@
lpdump \
mke2fs \
mkfs.erofs \
+ pbtombstone \
resize2fs \
sgdisk \
sqlite3 \
diff --git a/target/product/generic/Android.bp b/target/product/generic/Android.bp
index c980959..84db9e1 100644
--- a/target/product/generic/Android.bp
+++ b/target/product/generic/Android.bp
@@ -385,7 +385,6 @@
"android.software.webview.prebuilt.xml", // media_system
"android.software.window_magnification.prebuilt.xml", // handheld_system
"android.system.suspend-service",
- "prebuilt_vintf_manifest",
"apexd",
"appops",
"approved-ogki-builds.xml", // base_system
@@ -530,6 +529,7 @@
"storaged", // base_system
"surfaceflinger", // base_system
"svc", // base_system
+ "system_manifest.xml", // base_system
"task_profiles.json", // base_system
"tc", // base_system
"telecom", // base_system
@@ -863,11 +863,3 @@
},
},
}
-
-prebuilt_etc {
- name: "prebuilt_vintf_manifest",
- src: "manifest.xml",
- filename: "manifest.xml",
- relative_install_path: "vintf",
- no_full_install: true,
-}
diff --git a/target/product/generic/manifest.xml b/target/product/generic/manifest.xml
deleted file mode 100644
index 1df2c0d..0000000
--- a/target/product/generic/manifest.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<!--
- Input:
- system/libhidl/vintfdata/manifest.xml
--->
-<manifest version="8.0" type="framework">
- <hal format="hidl" max-level="6">
- <name>android.frameworks.displayservice</name>
- <transport>hwbinder</transport>
- <fqname>@1.0::IDisplayService/default</fqname>
- </hal>
- <hal format="hidl" max-level="5">
- <name>android.frameworks.schedulerservice</name>
- <transport>hwbinder</transport>
- <fqname>@1.0::ISchedulingPolicyService/default</fqname>
- </hal>
- <hal format="aidl">
- <name>android.frameworks.sensorservice</name>
- <fqname>ISensorManager/default</fqname>
- </hal>
- <hal format="hidl" max-level="8">
- <name>android.frameworks.sensorservice</name>
- <transport>hwbinder</transport>
- <fqname>@1.0::ISensorManager/default</fqname>
- </hal>
- <hal format="hidl" max-level="8">
- <name>android.hidl.memory</name>
- <transport arch="32+64">passthrough</transport>
- <fqname>@1.0::IMapper/ashmem</fqname>
- </hal>
- <hal format="hidl" max-level="7">
- <name>android.system.net.netd</name>
- <transport>hwbinder</transport>
- <fqname>@1.1::INetd/default</fqname>
- </hal>
- <hal format="hidl" max-level="7">
- <name>android.system.wifi.keystore</name>
- <transport>hwbinder</transport>
- <fqname>@1.0::IKeystore/default</fqname>
- </hal>
- <hal format="native">
- <name>netutils-wrapper</name>
- <version>1.0</version>
- </hal>
- <system-sdk>
- <version>29</version>
- <version>30</version>
- <version>31</version>
- <version>32</version>
- <version>33</version>
- <version>34</version>
- <version>35</version>
- <version>VanillaIceCream</version>
- </system-sdk>
-</manifest>
diff --git a/tools/aconfig/aconfig/Android.bp b/tools/aconfig/aconfig/Android.bp
index f4dd103..5e3eb12 100644
--- a/tools/aconfig/aconfig/Android.bp
+++ b/tools/aconfig/aconfig/Android.bp
@@ -68,6 +68,14 @@
],
}
+aconfig_values {
+ name: "aconfig.test.flag.second_values",
+ package: "com.android.aconfig.test",
+ srcs: [
+ "tests/third.values",
+ ],
+}
+
aconfig_value_set {
name: "aconfig.test.flag.value_set",
values: [
diff --git a/tools/aconfig/aconfig/src/codegen/java.rs b/tools/aconfig/aconfig/src/codegen/java.rs
index 81c7d00..067a3b4 100644
--- a/tools/aconfig/aconfig/src/codegen/java.rs
+++ b/tools/aconfig/aconfig/src/codegen/java.rs
@@ -501,7 +501,7 @@
modified_parsed_flags.into_iter(),
mode,
flag_ids,
- false,
+ true,
)
.unwrap();
let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
diff --git a/tools/aconfig/aconfig/src/commands.rs b/tools/aconfig/aconfig/src/commands.rs
index 496876e..0ad3d97 100644
--- a/tools/aconfig/aconfig/src/commands.rs
+++ b/tools/aconfig/aconfig/src/commands.rs
@@ -17,7 +17,7 @@
use anyhow::{bail, ensure, Context, Result};
use itertools::Itertools;
use protobuf::Message;
-use std::collections::{BTreeMap, HashMap};
+use std::collections::HashMap;
use std::hash::Hasher;
use std::io::Read;
use std::path::PathBuf;
@@ -425,23 +425,34 @@
#[allow(dead_code)] // TODO: b/316357686 - Use fingerprint in codegen to
// protect hardcoded offset reads.
-pub fn compute_flag_offsets_fingerprint(flags_map: &HashMap<String, u16>) -> Result<u64> {
+ // Creates a fingerprint of the flag names (which requires sorting the vector).
+ // Fingerprint is used by both codegen and storage files.
+pub fn compute_flags_fingerprint(flag_names: &mut Vec<String>) -> Result<u64> {
+ flag_names.sort();
+
let mut hasher = SipHasher13::new();
-
- // Need to sort to ensure the data is added to the hasher in the same order
- // each run.
- let sorted_map: BTreeMap<&String, &u16> = flags_map.iter().collect();
-
- for (flag, offset) in sorted_map {
- // See https://docs.rs/siphasher/latest/siphasher/#note for use of write
- // over write_i16. Similarly, use to_be_bytes rather than to_ne_bytes to
- // ensure consistency.
+ for flag in flag_names {
hasher.write(flag.as_bytes());
- hasher.write(&offset.to_be_bytes());
}
Ok(hasher.finish())
}
+#[allow(dead_code)] // TODO: b/316357686 - Use fingerprint in codegen to
+ // protect hardcoded offset reads.
+ // Converts ProtoParsedFlags into a vector of strings containing all of the flag
+ // names. Helper fn for creating fingerprint for codegen files. Flags must all
+ // belong to the same package.
+fn extract_flag_names(flags: ProtoParsedFlags) -> Result<Vec<String>> {
+ let separated_flags: Vec<ProtoParsedFlag> = flags.parsed_flag.into_iter().collect::<Vec<_>>();
+
+ // All flags must belong to the same package as the fingerprint is per-package.
+ let Some(_package) = find_unique_package(&separated_flags) else {
+ bail!("No parsed flags, or the parsed flags use different packages.");
+ };
+
+ Ok(separated_flags.into_iter().map(|flag| flag.name.unwrap()).collect::<Vec<_>>())
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -450,16 +461,51 @@
#[test]
fn test_offset_fingerprint() {
let parsed_flags = crate::test::parse_test_flags();
- let package = find_unique_package(&parsed_flags.parsed_flag).unwrap().to_string();
- let flag_ids = assign_flag_ids(&package, parsed_flags.parsed_flag.iter()).unwrap();
- let expected_fingerprint = 10709892481002252132u64;
+ let expected_fingerprint: u64 = 5801144784618221668;
- let hash_result = compute_flag_offsets_fingerprint(&flag_ids);
+ let mut extracted_flags = extract_flag_names(parsed_flags).unwrap();
+ let hash_result = compute_flags_fingerprint(&mut extracted_flags);
assert_eq!(hash_result.unwrap(), expected_fingerprint);
}
#[test]
+ fn test_offset_fingerprint_matches_from_package() {
+ let parsed_flags: ProtoParsedFlags = crate::test::parse_test_flags();
+
+ // All test flags are in the same package, so fingerprint from all of them.
+ let mut extracted_flags = extract_flag_names(parsed_flags.clone()).unwrap();
+ let result_from_parsed_flags = compute_flags_fingerprint(&mut extracted_flags);
+
+ let mut flag_names_vec = parsed_flags
+ .parsed_flag
+ .clone()
+ .into_iter()
+ .map(|flag| flag.name.unwrap())
+ .map(String::from)
+ .collect::<Vec<_>>();
+ let result_from_names = compute_flags_fingerprint(&mut flag_names_vec);
+
+ // Assert the same hash is generated for each case.
+ assert_eq!(result_from_parsed_flags.unwrap(), result_from_names.unwrap());
+ }
+
+ #[test]
+ fn test_offset_fingerprint_different_packages_does_not_match() {
+ // Parse flags from two packages.
+ let parsed_flags: ProtoParsedFlags = crate::test::parse_test_flags();
+ let second_parsed_flags = crate::test::parse_second_package_flags();
+
+ let mut extracted_flags = extract_flag_names(parsed_flags).unwrap();
+ let result_from_parsed_flags = compute_flags_fingerprint(&mut extracted_flags).unwrap();
+ let mut second_extracted_flags = extract_flag_names(second_parsed_flags).unwrap();
+ let second_result = compute_flags_fingerprint(&mut second_extracted_flags).unwrap();
+
+ // Different flags should have a different fingerprint.
+ assert_ne!(result_from_parsed_flags, second_result);
+ }
+
+ #[test]
fn test_parse_flags() {
let parsed_flags = crate::test::parse_test_flags(); // calls parse_flags
aconfig_protos::parsed_flags::verify_fields(&parsed_flags).unwrap();
diff --git a/tools/aconfig/aconfig/src/test.rs b/tools/aconfig/aconfig/src/test.rs
index 7409cda..a19b372 100644
--- a/tools/aconfig/aconfig/src/test.rs
+++ b/tools/aconfig/aconfig/src/test.rs
@@ -295,6 +295,24 @@
aconfig_protos::parsed_flags::try_from_binary_proto(&bytes).unwrap()
}
+ pub fn parse_second_package_flags() -> ProtoParsedFlags {
+ let bytes = crate::commands::parse_flags(
+ "com.android.aconfig.second_test",
+ Some("system"),
+ vec![Input {
+ source: "tests/test_second_package.aconfig".to_string(),
+ reader: Box::new(include_bytes!("../tests/test_second_package.aconfig").as_slice()),
+ }],
+ vec![Input {
+ source: "tests/third.values".to_string(),
+ reader: Box::new(include_bytes!("../tests/third.values").as_slice()),
+ }],
+ crate::commands::DEFAULT_FLAG_PERMISSION,
+ )
+ .unwrap();
+ aconfig_protos::parsed_flags::try_from_binary_proto(&bytes).unwrap()
+ }
+
pub fn first_significant_code_diff(a: &str, b: &str) -> Option<String> {
let a = a.lines().map(|line| line.trim_start()).filter(|line| !line.is_empty());
let b = b.lines().map(|line| line.trim_start()).filter(|line| !line.is_empty());
diff --git a/tools/aconfig/aconfig/templates/FeatureFlagsImpl.java.template b/tools/aconfig/aconfig/templates/FeatureFlagsImpl.java.template
index 8c7b3fa..15df902 100644
--- a/tools/aconfig/aconfig/templates/FeatureFlagsImpl.java.template
+++ b/tools/aconfig/aconfig/templates/FeatureFlagsImpl.java.template
@@ -1,5 +1,6 @@
package {package_name};
{{ -if not is_test_mode }}
+{{ -if allow_instrumentation }}
{{ if not library_exported- }}
// TODO(b/303773055): Remove the annotation after access issue is resolved.
import android.compat.annotation.UnsupportedAppUsage;
@@ -112,6 +113,72 @@
}
{{ endfor }}
}
+
+{{ else }} {#- else for allow_instrumentation is not enabled #}
+{{ if not library_exported- }}
+// TODO(b/303773055): Remove the annotation after access issue is resolved.
+import android.compat.annotation.UnsupportedAppUsage;
+{{ -endif }}
+
+{{ -if runtime_lookup_required }}
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+{{ -endif }}
+/** @hide */
+public final class FeatureFlagsImpl implements FeatureFlags \{
+{{ -if runtime_lookup_required }}
+{{ -for namespace_with_flags in namespace_flags }}
+ private static volatile boolean {namespace_with_flags.namespace}_is_cached = false;
+{{ -endfor- }}
+
+{{ for flag in flag_elements }}
+{{- if flag.is_read_write }}
+ private static boolean {flag.method_name} = {flag.default_value};
+{{ -endif }}
+{{ -endfor }}
+{{ for namespace_with_flags in namespace_flags }}
+ private void load_overrides_{namespace_with_flags.namespace}() \{
+ try \{
+ Properties properties = DeviceConfig.getProperties("{namespace_with_flags.namespace}");
+{{ -for flag in namespace_with_flags.flags }}
+{{ -if flag.is_read_write }}
+ {flag.method_name} =
+ properties.getBoolean(Flags.FLAG_{flag.flag_name_constant_suffix}, {flag.default_value});
+{{ -endif }}
+{{ -endfor }}
+ } catch (NullPointerException e) \{
+ throw new RuntimeException(
+ "Cannot read value from namespace {namespace_with_flags.namespace} "
+ + "from DeviceConfig. It could be that the code using flag "
+ + "executed before SettingsProvider initialization. Please use "
+ + "fixed read-only flag by adding is_fixed_read_only: true in "
+ + "flag declaration.",
+ e
+ );
+ }
+ {namespace_with_flags.namespace}_is_cached = true;
+}
+{{ endfor- }}
+{{ -endif }}{#- end of runtime_lookup_required #}
+{{ -for flag in flag_elements }}
+ @Override
+{{ -if not library_exported }}
+ @com.android.aconfig.annotations.AconfigFlagAccessor
+ @UnsupportedAppUsage
+{{ -endif }}
+ public boolean {flag.method_name}() \{
+{{ -if flag.is_read_write }}
+ if (!{flag.device_config_namespace}_is_cached) \{
+ load_overrides_{flag.device_config_namespace}();
+ }
+ return {flag.method_name};
+{{ -else }}
+ return {flag.default_value};
+{{ -endif }}
+ }
+{{ endfor }}
+}
+{{ endif}} {#- endif for allow_instrumentation #}
{{ else }} {#- Generate only stub if in test mode #}
/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags \{
diff --git a/tools/aconfig/aconfig/tests/test_second_package.aconfig b/tools/aconfig/aconfig/tests/test_second_package.aconfig
new file mode 100644
index 0000000..188bc96
--- /dev/null
+++ b/tools/aconfig/aconfig/tests/test_second_package.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.aconfig.second_test"
+container: "system"
+
+flag {
+ name: "testing_flag"
+ namespace: "another_namespace"
+ description: "This is a flag for testing."
+ bug: "123"
+}
+
diff --git a/tools/aconfig/aconfig/tests/third.values b/tools/aconfig/aconfig/tests/third.values
new file mode 100644
index 0000000..675832a
--- /dev/null
+++ b/tools/aconfig/aconfig/tests/third.values
@@ -0,0 +1,6 @@
+flag_value {
+ package: "com.android.aconfig.second_test"
+ name: "testing_flag"
+ state: DISABLED
+ permission: READ_WRITE
+}
diff --git a/tools/edit_monitor/Android.bp b/tools/edit_monitor/Android.bp
index e613563..b8ac5bf 100644
--- a/tools/edit_monitor/Android.bp
+++ b/tools/edit_monitor/Android.bp
@@ -36,6 +36,7 @@
srcs: [
"daemon_manager.py",
"edit_monitor.py",
+ "utils.py",
],
libs: [
"asuite_cc_client",
@@ -75,6 +76,21 @@
}
python_test_host {
+ name: "edit_monitor_utils_test",
+ main: "utils_test.py",
+ pkg_path: "edit_monitor",
+ srcs: [
+ "utils_test.py",
+ ],
+ libs: [
+ "edit_monitor_lib",
+ ],
+ test_options: {
+ unit_test: true,
+ },
+}
+
+python_test_host {
name: "edit_monitor_integration_test",
main: "edit_monitor_integration_test.py",
pkg_path: "testdata",
diff --git a/tools/edit_monitor/daemon_manager.py b/tools/edit_monitor/daemon_manager.py
index c0a57ab..9a0abb6 100644
--- a/tools/edit_monitor/daemon_manager.py
+++ b/tools/edit_monitor/daemon_manager.py
@@ -28,6 +28,7 @@
from atest.metrics import clearcut_client
from atest.proto import clientanalytics_pb2
+from edit_monitor import utils
from proto import edit_event_pb2
DEFAULT_PROCESS_TERMINATION_TIMEOUT_SECONDS = 5
@@ -79,6 +80,15 @@
def start(self):
"""Writes the pidfile and starts the daemon proces."""
+ if not utils.is_feature_enabled(
+ "edit_monitor",
+ self.user_name,
+ "ENABLE_EDIT_MONITOR",
+ "EDIT_MONITOR_ROLLOUT_PERCENTAGE",
+ ):
+ logging.warning("Edit monitor is disabled, exiting...")
+ return
+
if self.block_sign.exists():
logging.warning("Block sign found, exiting...")
return
diff --git a/tools/edit_monitor/daemon_manager_test.py b/tools/edit_monitor/daemon_manager_test.py
index e132000..407d94e 100644
--- a/tools/edit_monitor/daemon_manager_test.py
+++ b/tools/edit_monitor/daemon_manager_test.py
@@ -81,6 +81,8 @@
# Sets the tempdir under the working dir so any temp files created during
# tests will be cleaned.
tempfile.tempdir = self.working_dir.name
+ self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'})
+ self.patch.start()
def tearDown(self):
# Cleans up any child processes left by the tests.
@@ -88,6 +90,7 @@
self.working_dir.cleanup()
# Restores tempdir.
tempfile.tempdir = self.original_tempdir
+ self.patch.stop()
super().tearDown()
def test_start_success_with_no_existing_instance(self):
@@ -129,6 +132,15 @@
dm = daemon_manager.DaemonManager(TEST_BINARY_FILE)
dm.start()
+
+ # Verify no daemon process is started.
+ self.assertIsNone(dm.daemon_process)
+
+ @mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'false'}, clear=True)
+ def test_start_return_directly_if_disabled(self):
+ dm = daemon_manager.DaemonManager(TEST_BINARY_FILE)
+ dm.start()
+
# Verify no daemon process is started.
self.assertIsNone(dm.daemon_process)
@@ -137,6 +149,7 @@
'/google/cog/cloud/user/workspace/edit_monitor'
)
dm.start()
+
# Verify no daemon process is started.
self.assertIsNone(dm.daemon_process)
diff --git a/tools/edit_monitor/edit_monitor_integration_test.py b/tools/edit_monitor/edit_monitor_integration_test.py
index d7dc7f1..5f3d7e5 100644
--- a/tools/edit_monitor/edit_monitor_integration_test.py
+++ b/tools/edit_monitor/edit_monitor_integration_test.py
@@ -15,7 +15,6 @@
"""Integration tests for Edit Monitor."""
import glob
-from importlib import resources
import logging
import os
import pathlib
@@ -27,6 +26,9 @@
import time
import unittest
+from importlib import resources
+from unittest import mock
+
class EditMonitorIntegrationTest(unittest.TestCase):
@@ -46,8 +48,11 @@
)
self.root_monitoring_path.mkdir()
self.edit_monitor_binary_path = self._import_executable("edit_monitor")
+ self.patch = mock.patch.dict(os.environ, {'ENABLE_EDIT_MONITOR': 'true'})
+ self.patch.start()
def tearDown(self):
+ self.patch.stop()
self.working_dir.cleanup()
super().tearDown()
diff --git a/tools/edit_monitor/utils.py b/tools/edit_monitor/utils.py
new file mode 100644
index 0000000..1a3275c
--- /dev/null
+++ b/tools/edit_monitor/utils.py
@@ -0,0 +1,71 @@
+# Copyright 2024, 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.
+
+import hashlib
+import logging
+import os
+
+
+def is_feature_enabled(
+ feature_name: str,
+ user_name: str,
+ enable_flag: str = None,
+ rollout_flag: str = None,
+) -> bool:
+ """Determine whether the given feature is enabled.
+
+ Whether a given feature is enabled or not depends on two flags: 1) the
+ enable_flag that explicitly enable/disable the feature and 2) the rollout_flag
+ that controls the rollout percentage.
+
+ Args:
+ feature_name: name of the feature.
+ user_name: system user name.
+ enable_flag: name of the env var that enables/disables the feature
+ explicitly.
+ rollout_flg: name of the env var that controls the rollout percentage, the
+ value stored in the env var should be an int between 0 and 100 string
+ """
+ if enable_flag:
+ if os.environ.get(enable_flag, "") == "false":
+ logging.info("feature: %s is disabled", feature_name)
+ return False
+
+ if os.environ.get(enable_flag, "") == "true":
+ logging.info("feature: %s is enabled", feature_name)
+ return True
+
+ if not rollout_flag:
+ return True
+
+ hash_object = hashlib.sha256()
+ hash_object.update((user_name + feature_name).encode("utf-8"))
+ hash_number = int(hash_object.hexdigest(), 16) % 100
+
+ roll_out_percentage = os.environ.get(rollout_flag, "0")
+ try:
+ percentage = int(roll_out_percentage)
+ if percentage < 0 or percentage > 100:
+ logging.warning(
+ "Rollout percentage: %s out of range, disable the feature.",
+ roll_out_percentage,
+ )
+ return False
+ return hash_number < percentage
+ except ValueError:
+ logging.warning(
+ "Invalid rollout percentage: %s, disable the feature.",
+ roll_out_percentage,
+ )
+ return False
diff --git a/tools/edit_monitor/utils_test.py b/tools/edit_monitor/utils_test.py
new file mode 100644
index 0000000..7d7e4b2
--- /dev/null
+++ b/tools/edit_monitor/utils_test.py
@@ -0,0 +1,108 @@
+# Copyright 2024, 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.
+
+"""Unittests for edit monitor utils."""
+import os
+import unittest
+from unittest import mock
+
+from edit_monitor import utils
+
+TEST_USER = 'test_user'
+TEST_FEATURE = 'test_feature'
+ENABLE_TEST_FEATURE_FLAG = 'ENABLE_TEST_FEATURE'
+ROLLOUT_TEST_FEATURE_FLAG = 'ROLLOUT_TEST_FEATURE'
+
+
+class EnableFeatureTest(unittest.TestCase):
+
+ def test_feature_enabled_without_flag(self):
+ self.assertTrue(utils.is_feature_enabled(TEST_FEATURE, TEST_USER))
+
+ @mock.patch.dict(os.environ, {ENABLE_TEST_FEATURE_FLAG: 'false'}, clear=True)
+ def test_feature_disabled_with_flag(self):
+ self.assertFalse(
+ utils.is_feature_enabled(
+ TEST_FEATURE, TEST_USER, ENABLE_TEST_FEATURE_FLAG
+ )
+ )
+
+ @mock.patch.dict(os.environ, {ENABLE_TEST_FEATURE_FLAG: 'true'}, clear=True)
+ def test_feature_enabled_with_flag(self):
+ self.assertTrue(
+ utils.is_feature_enabled(
+ TEST_FEATURE, TEST_USER, ENABLE_TEST_FEATURE_FLAG
+ )
+ )
+
+ @mock.patch.dict(
+ os.environ, {ROLLOUT_TEST_FEATURE_FLAG: 'invalid'}, clear=True
+ )
+ def test_feature_disabled_with_invalid_rollout_percentage(self):
+ self.assertFalse(
+ utils.is_feature_enabled(
+ TEST_FEATURE,
+ TEST_USER,
+ ENABLE_TEST_FEATURE_FLAG,
+ ROLLOUT_TEST_FEATURE_FLAG,
+ )
+ )
+
+ @mock.patch.dict(os.environ, {ROLLOUT_TEST_FEATURE_FLAG: '101'}, clear=True)
+ def test_feature_disabled_with_rollout_percentage_too_high(self):
+ self.assertFalse(
+ utils.is_feature_enabled(
+ TEST_FEATURE,
+ TEST_USER,
+ ENABLE_TEST_FEATURE_FLAG,
+ ROLLOUT_TEST_FEATURE_FLAG,
+ )
+ )
+
+ @mock.patch.dict(os.environ, {ROLLOUT_TEST_FEATURE_FLAG: '-1'}, clear=True)
+ def test_feature_disabled_with_rollout_percentage_too_low(self):
+ self.assertFalse(
+ utils.is_feature_enabled(
+ TEST_FEATURE,
+ TEST_USER,
+ ENABLE_TEST_FEATURE_FLAG,
+ ROLLOUT_TEST_FEATURE_FLAG,
+ )
+ )
+
+ @mock.patch.dict(os.environ, {ROLLOUT_TEST_FEATURE_FLAG: '90'}, clear=True)
+ def test_feature_enabled_with_rollout_percentage(self):
+ self.assertTrue(
+ utils.is_feature_enabled(
+ TEST_FEATURE,
+ TEST_USER,
+ ENABLE_TEST_FEATURE_FLAG,
+ ROLLOUT_TEST_FEATURE_FLAG,
+ )
+ )
+
+ @mock.patch.dict(os.environ, {ROLLOUT_TEST_FEATURE_FLAG: '10'}, clear=True)
+ def test_feature_disabled_with_rollout_percentage(self):
+ self.assertFalse(
+ utils.is_feature_enabled(
+ TEST_FEATURE,
+ TEST_USER,
+ ENABLE_TEST_FEATURE_FLAG,
+ ROLLOUT_TEST_FEATURE_FLAG,
+ )
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()