aconfig: add java codegen test mode

Add java codegen test mode. The test mode will generate Flags.java and
FeatureFlagsImpl.java differently.
    * Flags.java will have getter and setter function to switch the
      FeatureFlagsImpl. Flags.java will not initialize the instance
      of FeatureFlagsImpl during initialization, thus it will force the
      user to set up the flag values for the tests.
    * FeatureFlagsImpl removes the dependency on DeviceConfig, and
      allows the caller to set the values of flags.

Command changes
This change adds a new parameter `mode` to `create-java-lib` subcommand.
The default value of `mode` is production, which will generate files for
production usage, and keeps the same behavior as before.

The new `mode` test is added to trigger the test mode. The command is
aconfig create-java-lib --cache=<path_to_cache> --out=<out_path>
--mode=test

Test: atest aconfig.test
Bug: 288632682
Change-Id: I7566464eb762f3107142fe787f56b17f5be631b7
This commit is contained in:
Zhi Dou
2023-06-26 21:03:40 +00:00
parent 571cd07796
commit 8ba6aa71b1
5 changed files with 221 additions and 55 deletions

View File

@@ -20,17 +20,23 @@ use std::path::PathBuf;
use tinytemplate::TinyTemplate; use tinytemplate::TinyTemplate;
use crate::codegen; use crate::codegen;
use crate::commands::OutputFile; use crate::commands::{CodegenMode, OutputFile};
use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag}; use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
pub fn generate_java_code<'a, I>(package: &str, parsed_flags_iter: I) -> Result<Vec<OutputFile>> pub fn generate_java_code<'a, I>(
package: &str,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
) -> Result<Vec<OutputFile>>
where where
I: Iterator<Item = &'a ProtoParsedFlag>, I: Iterator<Item = &'a ProtoParsedFlag>,
{ {
let class_elements: Vec<ClassElement> = let class_elements: Vec<ClassElement> =
parsed_flags_iter.map(|pf| create_class_element(package, pf)).collect(); 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 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(); let mut template = TinyTemplate::new();
template.add_template("Flags.java", include_str!("../templates/Flags.java.template"))?; template.add_template("Flags.java", include_str!("../templates/Flags.java.template"))?;
template.add_template( template.add_template(
@@ -56,14 +62,15 @@ where
#[derive(Serialize)] #[derive(Serialize)]
struct Context { struct Context {
pub package_name: String,
pub is_read_write: bool,
pub class_elements: Vec<ClassElement>, pub class_elements: Vec<ClassElement>,
pub is_test_mode: bool,
pub is_read_write: bool,
pub package_name: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct ClassElement { struct ClassElement {
pub default_value: String, pub default_value: bool,
pub device_config_namespace: String, pub device_config_namespace: String,
pub device_config_flag: String, pub device_config_flag: String,
pub flag_name_constant_suffix: 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()) let device_config_flag = codegen::create_device_config_ident(package, pf.name())
.expect("values checked at flag parse time"); .expect("values checked at flag parse time");
ClassElement { ClassElement {
default_value: if pf.state() == ProtoFlagState::ENABLED { default_value: pf.state() == ProtoFlagState::ENABLED,
"true".to_string()
} else {
"false".to_string()
},
device_config_namespace: pf.namespace().to_string(), device_config_namespace: pf.namespace().to_string(),
device_config_flag, device_config_flag,
flag_name_constant_suffix: pf.name().to_ascii_uppercase(), flag_name_constant_suffix: pf.name().to_ascii_uppercase(),
@@ -109,12 +112,16 @@ mod tests {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
#[test] const EXPECTED_FEATUREFLAGS_CONTENT: &str = r#"
fn test_generate_java_code() { package com.android.aconfig.test;
let parsed_flags = crate::test::parse_test_flags(); public interface FeatureFlags {
let generated_files = boolean disabledRo();
generate_java_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter()).unwrap(); boolean disabledRw();
let expect_flags_content = r#" boolean enabledRo();
boolean enabledRw();
}"#;
const EXPECTED_FLAG_COMMON_CONTENT: &str = r#"
package com.android.aconfig.test; package com.android.aconfig.test;
public final class Flags { public final class Flags {
public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro"; public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro";
@@ -134,9 +141,21 @@ mod tests {
public static boolean enabledRw() { public static boolean enabledRw() {
return FEATURE_FLAGS.enabledRw(); return FEATURE_FLAGS.enabledRw();
} }
private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
}
"#; "#;
#[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#" let expected_featureflagsimpl_content = r#"
package com.android.aconfig.test; package com.android.aconfig.test;
import android.provider.DeviceConfig; 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; package com.android.aconfig.test;
public interface FeatureFlags { import static java.util.stream.Collectors.toMap;
boolean disabledRo(); import java.util.stream.Stream;
boolean disabledRw(); import java.util.HashMap;
boolean enabledRo(); public final class FeatureFlagsImpl implements FeatureFlags {
boolean enabledRw(); @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<String, Boolean> 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([ 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/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 { for file in generated_files {

View File

@@ -129,12 +129,18 @@ pub fn parse_flags(package: &str, declarations: Vec<Input>, values: Vec<Input>)
Ok(output) Ok(output)
} }
pub fn create_java_lib(mut input: Input) -> Result<Vec<OutputFile>> { #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum CodegenMode {
Production,
Test,
}
pub fn create_java_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<Vec<OutputFile>> {
let parsed_flags = input.try_parse_flags()?; let parsed_flags = input.try_parse_flags()?;
let Some(package) = find_unique_package(&parsed_flags) else { let Some(package) = find_unique_package(&parsed_flags) else {
bail!("no parsed flags, or the parsed flags use different packages"); 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<OutputFile> { pub fn create_cpp_lib(mut input: Input) -> Result<OutputFile> {

View File

@@ -34,7 +34,7 @@ mod protos;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
use commands::{DumpFormat, Input, OutputFile}; use commands::{CodegenMode, DumpFormat, Input, OutputFile};
fn cli() -> Command { fn cli() -> Command {
Command::new("aconfig") Command::new("aconfig")
@@ -49,7 +49,13 @@ fn cli() -> Command {
.subcommand( .subcommand(
Command::new("create-java-lib") Command::new("create-java-lib")
.arg(Arg::new("cache").long("cache").required(true)) .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::<commands::CodegenMode>::new())
.default_value("production"),
),
) )
.subcommand( .subcommand(
Command::new("create-cpp-lib") Command::new("create-cpp-lib")
@@ -148,7 +154,8 @@ fn main() -> Result<()> {
} }
Some(("create-java-lib", sub_matches)) => { Some(("create-java-lib", sub_matches)) => {
let cache = open_single_file(sub_matches, "cache")?; let cache = open_single_file(sub_matches, "cache")?;
let generated_files = commands::create_java_lib(cache)?; let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
let generated_files = commands::create_java_lib(cache, *mode)?;
let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?); let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
generated_files generated_files
.iter() .iter()

View File

@@ -1,20 +1,61 @@
package {package_name}; 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; import android.provider.DeviceConfig;
{{ -endif- }}
{{ endif }} {{ endif }}
public final class FeatureFlagsImpl implements FeatureFlags \{ public final class FeatureFlagsImpl implements FeatureFlags \{
{{ for item in class_elements}} {{ for item in class_elements}}
@Override @Override
public boolean {item.method_name}() \{ public boolean {item.method_name}() \{
{{ if item.is_read_write- }} {{ -if not is_test_mode- }}
{{ if item.is_read_write }}
return DeviceConfig.getBoolean( return DeviceConfig.getBoolean(
"{item.device_config_namespace}", "{item.device_config_namespace}",
"{item.device_config_flag}", "{item.device_config_flag}",
{item.default_value} {item.default_value}
); );
{{ -else- }} {{ else }}
return {item.default_value}; return {item.default_value};
{{ -endif- }}
{{ else }}
return getFlag(Flags.FLAG_{item.flag_name_constant_suffix});
{{ -endif }} {{ -endif }}
} }
{{ endfor }} {{ endfor }}
{{ 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<String, Boolean> 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 }}
}

View File

@@ -9,6 +9,16 @@ public final class Flags \{
return FEATURE_FLAGS.{item.method_name}(); return FEATURE_FLAGS.{item.method_name}();
} }
{{ endfor }} {{ 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- }};
} }