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 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<Vec<OutputFile>>
pub fn generate_java_code<'a, I>(
package: &str,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
) -> Result<Vec<OutputFile>>
where
I: Iterator<Item = &'a ProtoParsedFlag>,
{
let class_elements: Vec<ClassElement> =
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<ClassElement>,
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<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([
("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 {

View file

@ -129,12 +129,18 @@ pub fn parse_flags(package: &str, declarations: Vec<Input>, values: Vec<Input>)
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 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<OutputFile> {

View file

@ -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::<commands::CodegenMode>::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::<CodegenMode>(sub_matches, "mode")?;
let generated_files = commands::create_java_lib(cache, *mode)?;
let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
generated_files
.iter()

View file

@ -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 }}
}
{{ 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}();
}
{{ 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- }};
}