diff --git a/tools/aconfig/src/codegen_java.rs b/tools/aconfig/src/codegen_java.rs index 47516b76c9..15eb2d65ba 100644 --- a/tools/aconfig/src/codegen_java.rs +++ b/tools/aconfig/src/codegen_java.rs @@ -20,17 +20,23 @@ use std::path::PathBuf; use tinytemplate::TinyTemplate; use crate::codegen; -use crate::commands::OutputFile; +use crate::commands::{CodegenMode, OutputFile}; use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag}; -pub fn generate_java_code<'a, I>(package: &str, parsed_flags_iter: I) -> Result> +pub fn generate_java_code<'a, I>( + package: &str, + parsed_flags_iter: I, + codegen_mode: CodegenMode, +) -> Result> where I: Iterator, { let class_elements: Vec = parsed_flags_iter.map(|pf| create_class_element(package, pf)).collect(); let is_read_write = class_elements.iter().any(|elem| elem.is_read_write); - let context = Context { package_name: package.to_string(), is_read_write, class_elements }; + let is_test_mode = codegen_mode == CodegenMode::Test; + let context = + Context { class_elements, is_test_mode, is_read_write, package_name: package.to_string() }; let mut template = TinyTemplate::new(); template.add_template("Flags.java", include_str!("../templates/Flags.java.template"))?; template.add_template( @@ -56,14 +62,15 @@ where #[derive(Serialize)] struct Context { - pub package_name: String, - pub is_read_write: bool, pub class_elements: Vec, + pub is_test_mode: bool, + pub is_read_write: bool, + pub package_name: String, } #[derive(Serialize)] struct ClassElement { - pub default_value: String, + pub default_value: bool, pub device_config_namespace: String, pub device_config_flag: String, pub flag_name_constant_suffix: String, @@ -75,11 +82,7 @@ fn create_class_element(package: &str, pf: &ProtoParsedFlag) -> ClassElement { let device_config_flag = codegen::create_device_config_ident(package, pf.name()) .expect("values checked at flag parse time"); ClassElement { - default_value: if pf.state() == ProtoFlagState::ENABLED { - "true".to_string() - } else { - "false".to_string() - }, + default_value: pf.state() == ProtoFlagState::ENABLED, device_config_namespace: pf.namespace().to_string(), device_config_flag, flag_name_constant_suffix: pf.name().to_ascii_uppercase(), @@ -109,34 +112,50 @@ mod tests { use super::*; use std::collections::HashMap; - #[test] - fn test_generate_java_code() { - let parsed_flags = crate::test::parse_test_flags(); - let generated_files = - generate_java_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter()).unwrap(); - let expect_flags_content = r#" - package com.android.aconfig.test; - public final class Flags { - public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro"; - public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw"; - public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro"; - public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw"; + const EXPECTED_FEATUREFLAGS_CONTENT: &str = r#" + package com.android.aconfig.test; + public interface FeatureFlags { + boolean disabledRo(); + boolean disabledRw(); + boolean enabledRo(); + boolean enabledRw(); + }"#; - public static boolean disabledRo() { - return FEATURE_FLAGS.disabledRo(); - } - public static boolean disabledRw() { - return FEATURE_FLAGS.disabledRw(); - } - public static boolean enabledRo() { - return FEATURE_FLAGS.enabledRo(); - } - public static boolean enabledRw() { - return FEATURE_FLAGS.enabledRw(); - } - private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); + const EXPECTED_FLAG_COMMON_CONTENT: &str = r#" + package com.android.aconfig.test; + public final class Flags { + public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro"; + public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw"; + public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro"; + public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw"; + + public static boolean disabledRo() { + return FEATURE_FLAGS.disabledRo(); } - "#; + public static boolean disabledRw() { + return FEATURE_FLAGS.disabledRw(); + } + public static boolean enabledRo() { + return FEATURE_FLAGS.enabledRo(); + } + public static boolean enabledRw() { + return FEATURE_FLAGS.enabledRw(); + } + "#; + + #[test] + fn test_generate_java_code_production() { + let parsed_flags = crate::test::parse_test_flags(); + let generated_files = generate_java_code( + crate::test::TEST_PACKAGE, + parsed_flags.parsed_flag.iter(), + CodegenMode::Production, + ) + .unwrap(); + let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string() + + r#" + private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); + }"#; let expected_featureflagsimpl_content = r#" package com.android.aconfig.test; import android.provider.DeviceConfig; @@ -167,19 +186,102 @@ mod tests { } } "#; - let expected_featureflags_content = r#" + let mut file_set = HashMap::from([ + ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()), + ("com/android/aconfig/test/FeatureFlagsImpl.java", expected_featureflagsimpl_content), + ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_CONTENT), + ]); + + for file in generated_files { + let file_path = file.path.to_str().unwrap(); + assert!(file_set.contains_key(file_path), "Cannot find {}", file_path); + assert_eq!( + None, + crate::test::first_significant_code_diff( + file_set.get(file_path).unwrap(), + &String::from_utf8(file.contents.clone()).unwrap() + ), + "File {} content is not correct", + file_path + ); + file_set.remove(file_path); + } + + assert!(file_set.is_empty()); + } + + #[test] + fn test_generate_java_code_test() { + let parsed_flags = crate::test::parse_test_flags(); + let generated_files = generate_java_code( + crate::test::TEST_PACKAGE, + parsed_flags.parsed_flag.iter(), + CodegenMode::Test, + ) + .unwrap(); + let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string() + + r#" + public static void setFeatureFlagsImpl(FeatureFlags featureFlags) { + Flags.FEATURE_FLAGS = featureFlags; + } + public static void unsetFeatureFlagsImpl() { + Flags.FEATURE_FLAGS = null; + } + private static FeatureFlags FEATURE_FLAGS; + } + "#; + let expected_featureflagsimpl_content = r#" package com.android.aconfig.test; - public interface FeatureFlags { - boolean disabledRo(); - boolean disabledRw(); - boolean enabledRo(); - boolean enabledRw(); + import static java.util.stream.Collectors.toMap; + import java.util.stream.Stream; + import java.util.HashMap; + public final class FeatureFlagsImpl implements FeatureFlags { + @Override + public boolean disabledRo() { + return getFlag(Flags.FLAG_DISABLED_RO); + } + @Override + public boolean disabledRw() { + return getFlag(Flags.FLAG_DISABLED_RW); + } + @Override + public boolean enabledRo() { + return getFlag(Flags.FLAG_ENABLED_RO); + } + @Override + public boolean enabledRw() { + return getFlag(Flags.FLAG_ENABLED_RW); + } + public void setFlag(String flagName, boolean value) { + if (!this.mFlagMap.containsKey(flagName)) { + throw new IllegalArgumentException("no such flag" + flagName); + } + this.mFlagMap.put(flagName, value); + } + private boolean getFlag(String flagName) { + Boolean value = this.mFlagMap.get(flagName); + if (value == null) { + throw new IllegalArgumentException(flagName + " is not set"); + } + return value; + } + private HashMap mFlagMap = Stream.of( + Flags.FLAG_DISABLED_RO, + Flags.FLAG_DISABLED_RW, + Flags.FLAG_ENABLED_RO, + Flags.FLAG_ENABLED_RW + ) + .collect( + HashMap::new, + (map, elem) -> map.put(elem, null), + HashMap::putAll + ); } "#; let mut file_set = HashMap::from([ - ("com/android/aconfig/test/Flags.java", expect_flags_content), + ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()), ("com/android/aconfig/test/FeatureFlagsImpl.java", expected_featureflagsimpl_content), - ("com/android/aconfig/test/FeatureFlags.java", expected_featureflags_content), + ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_CONTENT), ]); for file in generated_files { diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs index 58831ccf47..dd2087b3c3 100644 --- a/tools/aconfig/src/commands.rs +++ b/tools/aconfig/src/commands.rs @@ -129,12 +129,18 @@ pub fn parse_flags(package: &str, declarations: Vec, values: Vec) Ok(output) } -pub fn create_java_lib(mut input: Input) -> Result> { +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum CodegenMode { + Production, + Test, +} + +pub fn create_java_lib(mut input: Input, codegen_mode: CodegenMode) -> Result> { let parsed_flags = input.try_parse_flags()?; let Some(package) = find_unique_package(&parsed_flags) else { bail!("no parsed flags, or the parsed flags use different packages"); }; - generate_java_code(package, parsed_flags.parsed_flag.iter()) + generate_java_code(package, parsed_flags.parsed_flag.iter(), codegen_mode) } pub fn create_cpp_lib(mut input: Input) -> Result { diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs index e6a325dea7..e20c60cd2c 100644 --- a/tools/aconfig/src/main.rs +++ b/tools/aconfig/src/main.rs @@ -34,7 +34,7 @@ mod protos; #[cfg(test)] mod test; -use commands::{DumpFormat, Input, OutputFile}; +use commands::{CodegenMode, DumpFormat, Input, OutputFile}; fn cli() -> Command { Command::new("aconfig") @@ -49,7 +49,13 @@ fn cli() -> Command { .subcommand( Command::new("create-java-lib") .arg(Arg::new("cache").long("cache").required(true)) - .arg(Arg::new("out").long("out").required(true)), + .arg(Arg::new("out").long("out").required(true)) + .arg( + Arg::new("mode") + .long("mode") + .value_parser(EnumValueParser::::new()) + .default_value("production"), + ), ) .subcommand( Command::new("create-cpp-lib") @@ -148,7 +154,8 @@ fn main() -> Result<()> { } Some(("create-java-lib", sub_matches)) => { let cache = open_single_file(sub_matches, "cache")?; - let generated_files = commands::create_java_lib(cache)?; + let mode = get_required_arg::(sub_matches, "mode")?; + let generated_files = commands::create_java_lib(cache, *mode)?; let dir = PathBuf::from(get_required_arg::(sub_matches, "out")?); generated_files .iter() diff --git a/tools/aconfig/templates/FeatureFlagsImpl.java.template b/tools/aconfig/templates/FeatureFlagsImpl.java.template index 2b031f14ce..dafd99e71b 100644 --- a/tools/aconfig/templates/FeatureFlagsImpl.java.template +++ b/tools/aconfig/templates/FeatureFlagsImpl.java.template @@ -1,20 +1,61 @@ package {package_name}; -{{ if is_read_write }} +{{ -if is_test_mode }} +import static java.util.stream.Collectors.toMap; + +import java.util.stream.Stream; +import java.util.HashMap; +{{ else}} +{{ if is_read_write- }} import android.provider.DeviceConfig; +{{ -endif- }} {{ endif }} public final class FeatureFlagsImpl implements FeatureFlags \{ {{ for item in class_elements}} @Override public boolean {item.method_name}() \{ - {{ if item.is_read_write- }} + {{ -if not is_test_mode- }} + {{ if item.is_read_write }} return DeviceConfig.getBoolean( "{item.device_config_namespace}", "{item.device_config_flag}", {item.default_value} ); - {{ -else- }} + {{ else }} return {item.default_value}; + {{ -endif- }} + {{ else }} + return getFlag(Flags.FLAG_{item.flag_name_constant_suffix}); {{ -endif }} } {{ endfor }} -} \ No newline at end of file + + {{ if is_test_mode- }} + public void setFlag(String flagName, boolean value) \{ + if (!this.mFlagMap.containsKey(flagName)) \{ + throw new IllegalArgumentException("no such flag" + flagName); + } + this.mFlagMap.put(flagName, value); + } + + private boolean getFlag(String flagName) \{ + Boolean value = this.mFlagMap.get(flagName); + if (value == null) \{ + throw new IllegalArgumentException(flagName + " is not set"); + } + return value; + } + + private HashMap mFlagMap = Stream.of( + {{-for item in class_elements}} + Flags.FLAG_{item.flag_name_constant_suffix}{{ if not @last }},{{ endif }} + {{ -endfor }} + ) + .collect( + HashMap::new, + (map, elem) -> map.put(elem, null), + HashMap::putAll + ); + {{ -endif }} +} + + diff --git a/tools/aconfig/templates/Flags.java.template b/tools/aconfig/templates/Flags.java.template index 62116c59e2..eef98eb635 100644 --- a/tools/aconfig/templates/Flags.java.template +++ b/tools/aconfig/templates/Flags.java.template @@ -9,6 +9,16 @@ public final class Flags \{ return FEATURE_FLAGS.{item.method_name}(); } {{ endfor }} - private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl(); + {{ if is_test_mode }} + public static void setFeatureFlagsImpl(FeatureFlags featureFlags) \{ + Flags.FEATURE_FLAGS = featureFlags; + } + + public static void unsetFeatureFlagsImpl() \{ + Flags.FEATURE_FLAGS = null; + } + {{ -endif}} + + private static FeatureFlags FEATURE_FLAGS{{ -if not is_test_mode }} = new FeatureFlagsImpl(){{ -endif- }}; }