• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 use anyhow::Result;
18 use serde::Serialize;
19 use std::collections::{BTreeMap, BTreeSet};
20 use std::path::PathBuf;
21 use tinytemplate::TinyTemplate;
22 
23 use aconfig_protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
24 
25 use crate::codegen;
26 use crate::codegen::CodegenMode;
27 use crate::commands::OutputFile;
28 
generate_java_code<I>( package: &str, parsed_flags_iter: I, codegen_mode: CodegenMode, ) -> Result<Vec<OutputFile>> where I: Iterator<Item = ProtoParsedFlag>,29 pub fn generate_java_code<I>(
30     package: &str,
31     parsed_flags_iter: I,
32     codegen_mode: CodegenMode,
33 ) -> Result<Vec<OutputFile>>
34 where
35     I: Iterator<Item = ProtoParsedFlag>,
36 {
37     let flag_elements: Vec<FlagElement> =
38         parsed_flags_iter.map(|pf| create_flag_element(package, &pf)).collect();
39     let namespace_flags = gen_flags_by_namespace(&flag_elements);
40     let properties_set: BTreeSet<String> =
41         flag_elements.iter().map(|fe| format_property_name(&fe.device_config_namespace)).collect();
42     let is_test_mode = codegen_mode == CodegenMode::Test;
43     let library_exported = codegen_mode == CodegenMode::Exported;
44     let runtime_lookup_required =
45         flag_elements.iter().any(|elem| elem.is_read_write) || library_exported;
46 
47     let context = Context {
48         flag_elements,
49         namespace_flags,
50         is_test_mode,
51         runtime_lookup_required,
52         properties_set,
53         package_name: package.to_string(),
54         library_exported,
55     };
56     let mut template = TinyTemplate::new();
57     template.add_template("Flags.java", include_str!("../../templates/Flags.java.template"))?;
58     template.add_template(
59         "FeatureFlagsImpl.java",
60         include_str!("../../templates/FeatureFlagsImpl.java.template"),
61     )?;
62     template.add_template(
63         "FeatureFlags.java",
64         include_str!("../../templates/FeatureFlags.java.template"),
65     )?;
66     template.add_template(
67         "CustomFeatureFlags.java",
68         include_str!("../../templates/CustomFeatureFlags.java.template"),
69     )?;
70     template.add_template(
71         "FakeFeatureFlagsImpl.java",
72         include_str!("../../templates/FakeFeatureFlagsImpl.java.template"),
73     )?;
74 
75     let path: PathBuf = package.split('.').collect();
76     [
77         "Flags.java",
78         "FeatureFlags.java",
79         "FeatureFlagsImpl.java",
80         "CustomFeatureFlags.java",
81         "FakeFeatureFlagsImpl.java",
82     ]
83     .iter()
84     .map(|file| {
85         Ok(OutputFile { contents: template.render(file, &context)?.into(), path: path.join(file) })
86     })
87     .collect::<Result<Vec<OutputFile>>>()
88 }
89 
gen_flags_by_namespace(flags: &[FlagElement]) -> Vec<NamespaceFlags>90 fn gen_flags_by_namespace(flags: &[FlagElement]) -> Vec<NamespaceFlags> {
91     let mut namespace_to_flag: BTreeMap<String, Vec<FlagElement>> = BTreeMap::new();
92 
93     for flag in flags {
94         match namespace_to_flag.get_mut(&flag.device_config_namespace) {
95             Some(flag_list) => flag_list.push(flag.clone()),
96             None => {
97                 namespace_to_flag.insert(flag.device_config_namespace.clone(), vec![flag.clone()]);
98             }
99         }
100     }
101 
102     namespace_to_flag
103         .iter()
104         .map(|(namespace, flags)| NamespaceFlags {
105             namespace: namespace.to_string(),
106             flags: flags.clone(),
107         })
108         .collect()
109 }
110 
111 #[derive(Serialize)]
112 struct Context {
113     pub flag_elements: Vec<FlagElement>,
114     pub namespace_flags: Vec<NamespaceFlags>,
115     pub is_test_mode: bool,
116     pub runtime_lookup_required: bool,
117     pub properties_set: BTreeSet<String>,
118     pub package_name: String,
119     pub library_exported: bool,
120 }
121 
122 #[derive(Serialize, Debug)]
123 struct NamespaceFlags {
124     pub namespace: String,
125     pub flags: Vec<FlagElement>,
126 }
127 
128 #[derive(Serialize, Clone, Debug)]
129 struct FlagElement {
130     pub default_value: bool,
131     pub device_config_namespace: String,
132     pub device_config_flag: String,
133     pub flag_name_constant_suffix: String,
134     pub is_read_write: bool,
135     pub method_name: String,
136     pub properties: String,
137 }
138 
create_flag_element(package: &str, pf: &ProtoParsedFlag) -> FlagElement139 fn create_flag_element(package: &str, pf: &ProtoParsedFlag) -> FlagElement {
140     let device_config_flag = codegen::create_device_config_ident(package, pf.name())
141         .expect("values checked at flag parse time");
142     FlagElement {
143         default_value: pf.state() == ProtoFlagState::ENABLED,
144         device_config_namespace: pf.namespace().to_string(),
145         device_config_flag,
146         flag_name_constant_suffix: pf.name().to_ascii_uppercase(),
147         is_read_write: pf.permission() == ProtoFlagPermission::READ_WRITE,
148         method_name: format_java_method_name(pf.name()),
149         properties: format_property_name(pf.namespace()),
150     }
151 }
152 
format_java_method_name(flag_name: &str) -> String153 fn format_java_method_name(flag_name: &str) -> String {
154     let splits: Vec<&str> = flag_name.split('_').filter(|&word| !word.is_empty()).collect();
155     if splits.len() == 1 {
156         let name = splits[0];
157         name[0..1].to_ascii_lowercase() + &name[1..]
158     } else {
159         splits
160             .iter()
161             .enumerate()
162             .map(|(index, word)| {
163                 if index == 0 {
164                     word.to_ascii_lowercase()
165                 } else {
166                     word[0..1].to_ascii_uppercase() + &word[1..].to_ascii_lowercase()
167                 }
168             })
169             .collect::<Vec<String>>()
170             .join("")
171     }
172 }
173 
format_property_name(property_name: &str) -> String174 fn format_property_name(property_name: &str) -> String {
175     let name = format_java_method_name(property_name);
176     format!("mProperties{}{}", &name[0..1].to_ascii_uppercase(), &name[1..])
177 }
178 
179 #[cfg(test)]
180 mod tests {
181     use super::*;
182     use std::collections::HashMap;
183 
184     const EXPECTED_FEATUREFLAGS_COMMON_CONTENT: &str = r#"
185     package com.android.aconfig.test;
186     // TODO(b/303773055): Remove the annotation after access issue is resolved.
187     import android.compat.annotation.UnsupportedAppUsage;
188     /** @hide */
189     public interface FeatureFlags {
190         @com.android.aconfig.annotations.AssumeFalseForR8
191         @com.android.aconfig.annotations.AconfigFlagAccessor
192         @UnsupportedAppUsage
193         boolean disabledRo();
194         @com.android.aconfig.annotations.AconfigFlagAccessor
195         @UnsupportedAppUsage
196         boolean disabledRw();
197         @com.android.aconfig.annotations.AconfigFlagAccessor
198         @UnsupportedAppUsage
199         boolean disabledRwExported();
200         @com.android.aconfig.annotations.AconfigFlagAccessor
201         @UnsupportedAppUsage
202         boolean disabledRwInOtherNamespace();
203         @com.android.aconfig.annotations.AssumeTrueForR8
204         @com.android.aconfig.annotations.AconfigFlagAccessor
205         @UnsupportedAppUsage
206         boolean enabledFixedRo();
207         @com.android.aconfig.annotations.AssumeTrueForR8
208         @com.android.aconfig.annotations.AconfigFlagAccessor
209         @UnsupportedAppUsage
210         boolean enabledFixedRoExported();
211         @com.android.aconfig.annotations.AssumeTrueForR8
212         @com.android.aconfig.annotations.AconfigFlagAccessor
213         @UnsupportedAppUsage
214         boolean enabledRo();
215         @com.android.aconfig.annotations.AssumeTrueForR8
216         @com.android.aconfig.annotations.AconfigFlagAccessor
217         @UnsupportedAppUsage
218         boolean enabledRoExported();
219         @com.android.aconfig.annotations.AconfigFlagAccessor
220         @UnsupportedAppUsage
221         boolean enabledRw();
222     }
223     "#;
224 
225     const EXPECTED_FLAG_COMMON_CONTENT: &str = r#"
226     package com.android.aconfig.test;
227     // TODO(b/303773055): Remove the annotation after access issue is resolved.
228     import android.compat.annotation.UnsupportedAppUsage;
229     /** @hide */
230     public final class Flags {
231         /** @hide */
232         public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro";
233         /** @hide */
234         public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw";
235         /** @hide */
236         public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported";
237         /** @hide */
238         public static final String FLAG_DISABLED_RW_IN_OTHER_NAMESPACE = "com.android.aconfig.test.disabled_rw_in_other_namespace";
239         /** @hide */
240         public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro";
241         /** @hide */
242         public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
243         /** @hide */
244         public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro";
245         /** @hide */
246         public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
247         /** @hide */
248         public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw";
249 
250         @com.android.aconfig.annotations.AssumeFalseForR8
251         @com.android.aconfig.annotations.AconfigFlagAccessor
252         @UnsupportedAppUsage
253         public static boolean disabledRo() {
254             return FEATURE_FLAGS.disabledRo();
255         }
256         @com.android.aconfig.annotations.AconfigFlagAccessor
257         @UnsupportedAppUsage
258         public static boolean disabledRw() {
259             return FEATURE_FLAGS.disabledRw();
260         }
261         @com.android.aconfig.annotations.AconfigFlagAccessor
262         @UnsupportedAppUsage
263         public static boolean disabledRwExported() {
264             return FEATURE_FLAGS.disabledRwExported();
265         }
266         @com.android.aconfig.annotations.AconfigFlagAccessor
267         @UnsupportedAppUsage
268         public static boolean disabledRwInOtherNamespace() {
269             return FEATURE_FLAGS.disabledRwInOtherNamespace();
270         }
271         @com.android.aconfig.annotations.AssumeTrueForR8
272         @com.android.aconfig.annotations.AconfigFlagAccessor
273         @UnsupportedAppUsage
274         public static boolean enabledFixedRo() {
275             return FEATURE_FLAGS.enabledFixedRo();
276         }
277         @com.android.aconfig.annotations.AssumeTrueForR8
278         @com.android.aconfig.annotations.AconfigFlagAccessor
279         @UnsupportedAppUsage
280         public static boolean enabledFixedRoExported() {
281             return FEATURE_FLAGS.enabledFixedRoExported();
282         }
283         @com.android.aconfig.annotations.AssumeTrueForR8
284         @com.android.aconfig.annotations.AconfigFlagAccessor
285         @UnsupportedAppUsage
286         public static boolean enabledRo() {
287             return FEATURE_FLAGS.enabledRo();
288         }
289         @com.android.aconfig.annotations.AssumeTrueForR8
290         @com.android.aconfig.annotations.AconfigFlagAccessor
291         @UnsupportedAppUsage
292         public static boolean enabledRoExported() {
293             return FEATURE_FLAGS.enabledRoExported();
294         }
295         @com.android.aconfig.annotations.AconfigFlagAccessor
296         @UnsupportedAppUsage
297         public static boolean enabledRw() {
298             return FEATURE_FLAGS.enabledRw();
299         }
300     "#;
301 
302     const EXPECTED_CUSTOMFEATUREFLAGS_CONTENT: &str = r#"
303     package com.android.aconfig.test;
304 
305     // TODO(b/303773055): Remove the annotation after access issue is resolved.
306     import android.compat.annotation.UnsupportedAppUsage;
307     import java.util.Arrays;
308     import java.util.HashSet;
309     import java.util.List;
310     import java.util.Set;
311     import java.util.function.BiPredicate;
312     import java.util.function.Predicate;
313 
314     /** @hide */
315     public class CustomFeatureFlags implements FeatureFlags {
316 
317         private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl;
318 
319         public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) {
320             mGetValueImpl = getValueImpl;
321         }
322 
323         @Override
324         @UnsupportedAppUsage
325         public boolean disabledRo() {
326             return getValue(Flags.FLAG_DISABLED_RO,
327                     FeatureFlags::disabledRo);
328         }
329         @Override
330         @UnsupportedAppUsage
331         public boolean disabledRw() {
332             return getValue(Flags.FLAG_DISABLED_RW,
333                 FeatureFlags::disabledRw);
334         }
335         @Override
336         @UnsupportedAppUsage
337         public boolean disabledRwExported() {
338             return getValue(Flags.FLAG_DISABLED_RW_EXPORTED,
339                 FeatureFlags::disabledRwExported);
340         }
341         @Override
342         @UnsupportedAppUsage
343         public boolean disabledRwInOtherNamespace() {
344             return getValue(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE,
345                 FeatureFlags::disabledRwInOtherNamespace);
346         }
347         @Override
348         @UnsupportedAppUsage
349         public boolean enabledFixedRo() {
350             return getValue(Flags.FLAG_ENABLED_FIXED_RO,
351                 FeatureFlags::enabledFixedRo);
352         }
353         @Override
354         @UnsupportedAppUsage
355         public boolean enabledFixedRoExported() {
356             return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED,
357                 FeatureFlags::enabledFixedRoExported);
358         }
359         @Override
360         @UnsupportedAppUsage
361         public boolean enabledRo() {
362             return getValue(Flags.FLAG_ENABLED_RO,
363                 FeatureFlags::enabledRo);
364         }
365         @Override
366         @UnsupportedAppUsage
367         public boolean enabledRoExported() {
368             return getValue(Flags.FLAG_ENABLED_RO_EXPORTED,
369                 FeatureFlags::enabledRoExported);
370         }
371         @Override
372         @UnsupportedAppUsage
373         public boolean enabledRw() {
374             return getValue(Flags.FLAG_ENABLED_RW,
375                 FeatureFlags::enabledRw);
376         }
377 
378         public boolean isFlagReadOnlyOptimized(String flagName) {
379             if (mReadOnlyFlagsSet.contains(flagName) &&
380                 isOptimizationEnabled()) {
381                     return true;
382             }
383             return false;
384         }
385 
386         @com.android.aconfig.annotations.AssumeTrueForR8
387         private boolean isOptimizationEnabled() {
388             return false;
389         }
390 
391         protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) {
392             return mGetValueImpl.test(flagName, getter);
393         }
394 
395         public List<String> getFlagNames() {
396             return Arrays.asList(
397                 Flags.FLAG_DISABLED_RO,
398                 Flags.FLAG_DISABLED_RW,
399                 Flags.FLAG_DISABLED_RW_EXPORTED,
400                 Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE,
401                 Flags.FLAG_ENABLED_FIXED_RO,
402                 Flags.FLAG_ENABLED_FIXED_RO_EXPORTED,
403                 Flags.FLAG_ENABLED_RO,
404                 Flags.FLAG_ENABLED_RO_EXPORTED,
405                 Flags.FLAG_ENABLED_RW
406             );
407         }
408 
409         private Set<String> mReadOnlyFlagsSet = new HashSet<>(
410             Arrays.asList(
411                 Flags.FLAG_DISABLED_RO,
412                 Flags.FLAG_ENABLED_FIXED_RO,
413                 Flags.FLAG_ENABLED_FIXED_RO_EXPORTED,
414                 Flags.FLAG_ENABLED_RO,
415                 Flags.FLAG_ENABLED_RO_EXPORTED,
416                 ""
417             )
418         );
419     }
420     "#;
421 
422     const EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT: &str = r#"
423     package com.android.aconfig.test;
424 
425     import java.util.HashMap;
426     import java.util.Map;
427     import java.util.function.Predicate;
428 
429     /** @hide */
430     public class FakeFeatureFlagsImpl extends CustomFeatureFlags {
431         private final Map<String, Boolean> mFlagMap = new HashMap<>();
432         private final FeatureFlags mDefaults;
433 
434         public FakeFeatureFlagsImpl() {
435             this(null);
436         }
437 
438         public FakeFeatureFlagsImpl(FeatureFlags defaults) {
439             super(null);
440             mDefaults = defaults;
441             // Initialize the map with null values
442             for (String flagName : getFlagNames()) {
443                 mFlagMap.put(flagName, null);
444             }
445         }
446 
447         @Override
448         protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) {
449             Boolean value = this.mFlagMap.get(flagName);
450             if (value != null) {
451                 return value;
452             }
453             if (mDefaults != null) {
454                 return getter.test(mDefaults);
455             }
456             throw new IllegalArgumentException(flagName + " is not set");
457         }
458 
459         public void setFlag(String flagName, boolean value) {
460             if (!this.mFlagMap.containsKey(flagName)) {
461                 throw new IllegalArgumentException("no such flag " + flagName);
462             }
463             this.mFlagMap.put(flagName, value);
464         }
465 
466         public void resetAll() {
467             for (Map.Entry entry : mFlagMap.entrySet()) {
468                 entry.setValue(null);
469             }
470         }
471     }
472     "#;
473 
474     #[test]
test_generate_java_code_production()475     fn test_generate_java_code_production() {
476         let parsed_flags = crate::test::parse_test_flags();
477         let mode = CodegenMode::Production;
478         let modified_parsed_flags =
479             crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
480         let generated_files =
481             generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
482                 .unwrap();
483         let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
484             + r#"
485             private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
486         }"#;
487 
488         let expect_featureflagsimpl_content = r#"
489         package com.android.aconfig.test;
490         // TODO(b/303773055): Remove the annotation after access issue is resolved.
491         import android.compat.annotation.UnsupportedAppUsage;
492         import android.provider.DeviceConfig;
493         import android.provider.DeviceConfig.Properties;
494         /** @hide */
495         public final class FeatureFlagsImpl implements FeatureFlags {
496             private static boolean aconfig_test_is_cached = false;
497             private static boolean other_namespace_is_cached = false;
498             private static boolean disabledRw = false;
499             private static boolean disabledRwExported = false;
500             private static boolean disabledRwInOtherNamespace = false;
501             private static boolean enabledRw = true;
502 
503 
504             private void load_overrides_aconfig_test() {
505                 try {
506                     Properties properties = DeviceConfig.getProperties("aconfig_test");
507                     disabledRw =
508                         properties.getBoolean("com.android.aconfig.test.disabled_rw", false);
509                     disabledRwExported =
510                         properties.getBoolean("com.android.aconfig.test.disabled_rw_exported", false);
511                     enabledRw =
512                         properties.getBoolean("com.android.aconfig.test.enabled_rw", true);
513                 } catch (NullPointerException e) {
514                     throw new RuntimeException(
515                         "Cannot read value from namespace aconfig_test "
516                         + "from DeviceConfig. It could be that the code using flag "
517                         + "executed before SettingsProvider initialization. Please use "
518                         + "fixed read-only flag by adding is_fixed_read_only: true in "
519                         + "flag declaration.",
520                         e
521                     );
522                 }
523                 aconfig_test_is_cached = true;
524             }
525 
526             private void load_overrides_other_namespace() {
527                 try {
528                     Properties properties = DeviceConfig.getProperties("other_namespace");
529                     disabledRwInOtherNamespace =
530                         properties.getBoolean("com.android.aconfig.test.disabled_rw_in_other_namespace", false);
531                 } catch (NullPointerException e) {
532                     throw new RuntimeException(
533                         "Cannot read value from namespace other_namespace "
534                         + "from DeviceConfig. It could be that the code using flag "
535                         + "executed before SettingsProvider initialization. Please use "
536                         + "fixed read-only flag by adding is_fixed_read_only: true in "
537                         + "flag declaration.",
538                         e
539                     );
540                 }
541                 other_namespace_is_cached = true;
542             }
543 
544             @Override
545             @com.android.aconfig.annotations.AconfigFlagAccessor
546             @UnsupportedAppUsage
547             public boolean disabledRo() {
548                 return false;
549             }
550             @Override
551             @com.android.aconfig.annotations.AconfigFlagAccessor
552             @UnsupportedAppUsage
553             public boolean disabledRw() {
554                 if (!aconfig_test_is_cached) {
555                     load_overrides_aconfig_test();
556                 }
557                 return disabledRw;
558             }
559             @Override
560             @com.android.aconfig.annotations.AconfigFlagAccessor
561             @UnsupportedAppUsage
562             public boolean disabledRwExported() {
563                 if (!aconfig_test_is_cached) {
564                     load_overrides_aconfig_test();
565                 }
566                 return disabledRwExported;
567             }
568             @Override
569             @com.android.aconfig.annotations.AconfigFlagAccessor
570             @UnsupportedAppUsage
571             public boolean disabledRwInOtherNamespace() {
572                 if (!other_namespace_is_cached) {
573                     load_overrides_other_namespace();
574                 }
575                 return disabledRwInOtherNamespace;
576             }
577             @Override
578             @com.android.aconfig.annotations.AconfigFlagAccessor
579             @UnsupportedAppUsage
580             public boolean enabledFixedRo() {
581                 return true;
582             }
583             @Override
584             @com.android.aconfig.annotations.AconfigFlagAccessor
585             @UnsupportedAppUsage
586             public boolean enabledFixedRoExported() {
587                 return true;
588             }
589             @Override
590             @com.android.aconfig.annotations.AconfigFlagAccessor
591             @UnsupportedAppUsage
592             public boolean enabledRo() {
593                 return true;
594             }
595             @Override
596             @com.android.aconfig.annotations.AconfigFlagAccessor
597             @UnsupportedAppUsage
598             public boolean enabledRoExported() {
599                 return true;
600             }
601             @Override
602             @com.android.aconfig.annotations.AconfigFlagAccessor
603             @UnsupportedAppUsage
604             public boolean enabledRw() {
605                 if (!aconfig_test_is_cached) {
606                     load_overrides_aconfig_test();
607                 }
608                 return enabledRw;
609             }
610         }
611         "#;
612         let mut file_set = HashMap::from([
613             ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()),
614             ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content),
615             ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_COMMON_CONTENT),
616             (
617                 "com/android/aconfig/test/CustomFeatureFlags.java",
618                 EXPECTED_CUSTOMFEATUREFLAGS_CONTENT,
619             ),
620             (
621                 "com/android/aconfig/test/FakeFeatureFlagsImpl.java",
622                 EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT,
623             ),
624         ]);
625 
626         for file in generated_files {
627             let file_path = file.path.to_str().unwrap();
628             assert!(file_set.contains_key(file_path), "Cannot find {}", file_path);
629             assert_eq!(
630                 None,
631                 crate::test::first_significant_code_diff(
632                     file_set.get(file_path).unwrap(),
633                     &String::from_utf8(file.contents).unwrap()
634                 ),
635                 "File {} content is not correct",
636                 file_path
637             );
638             file_set.remove(file_path);
639         }
640 
641         assert!(file_set.is_empty());
642     }
643 
644     #[test]
test_generate_java_code_exported()645     fn test_generate_java_code_exported() {
646         let parsed_flags = crate::test::parse_test_flags();
647         let mode = CodegenMode::Exported;
648         let modified_parsed_flags =
649             crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
650         let generated_files =
651             generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
652                 .unwrap();
653 
654         let expect_flags_content = r#"
655         package com.android.aconfig.test;
656         /** @hide */
657         public final class Flags {
658             /** @hide */
659             public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported";
660             /** @hide */
661             public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
662             /** @hide */
663             public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
664             public static boolean disabledRwExported() {
665                 return FEATURE_FLAGS.disabledRwExported();
666             }
667             public static boolean enabledFixedRoExported() {
668                 return FEATURE_FLAGS.enabledFixedRoExported();
669             }
670             public static boolean enabledRoExported() {
671                 return FEATURE_FLAGS.enabledRoExported();
672             }
673             private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
674         }
675         "#;
676 
677         let expect_feature_flags_content = r#"
678         package com.android.aconfig.test;
679         /** @hide */
680         public interface FeatureFlags {
681             boolean disabledRwExported();
682             boolean enabledFixedRoExported();
683             boolean enabledRoExported();
684         }
685         "#;
686 
687         let expect_feature_flags_impl_content = r#"
688         package com.android.aconfig.test;
689         import android.provider.DeviceConfig;
690         import android.provider.DeviceConfig.Properties;
691         /** @hide */
692         public final class FeatureFlagsImpl implements FeatureFlags {
693             private static boolean aconfig_test_is_cached = false;
694             private static boolean disabledRwExported = false;
695             private static boolean enabledFixedRoExported = false;
696             private static boolean enabledRoExported = false;
697 
698 
699             private void load_overrides_aconfig_test() {
700                 try {
701                     Properties properties = DeviceConfig.getProperties("aconfig_test");
702                     disabledRwExported =
703                         properties.getBoolean("com.android.aconfig.test.disabled_rw_exported", false);
704                     enabledFixedRoExported =
705                         properties.getBoolean("com.android.aconfig.test.enabled_fixed_ro_exported", false);
706                     enabledRoExported =
707                         properties.getBoolean("com.android.aconfig.test.enabled_ro_exported", false);
708                 } catch (NullPointerException e) {
709                     throw new RuntimeException(
710                         "Cannot read value from namespace aconfig_test "
711                         + "from DeviceConfig. It could be that the code using flag "
712                         + "executed before SettingsProvider initialization. Please use "
713                         + "fixed read-only flag by adding is_fixed_read_only: true in "
714                         + "flag declaration.",
715                         e
716                     );
717                 }
718                 aconfig_test_is_cached = true;
719             }
720             @Override
721             public boolean disabledRwExported() {
722                 if (!aconfig_test_is_cached) {
723                     load_overrides_aconfig_test();
724                 }
725                 return disabledRwExported;
726             }
727             @Override
728             public boolean enabledFixedRoExported() {
729                 if (!aconfig_test_is_cached) {
730                     load_overrides_aconfig_test();
731                 }
732                 return enabledFixedRoExported;
733             }
734             @Override
735             public boolean enabledRoExported() {
736                 if (!aconfig_test_is_cached) {
737                     load_overrides_aconfig_test();
738                 }
739                 return enabledRoExported;
740             }
741         }"#;
742 
743         let expect_custom_feature_flags_content = r#"
744         package com.android.aconfig.test;
745 
746         import java.util.Arrays;
747         import java.util.HashSet;
748         import java.util.List;
749         import java.util.Set;
750         import java.util.function.BiPredicate;
751         import java.util.function.Predicate;
752 
753         /** @hide */
754         public class CustomFeatureFlags implements FeatureFlags {
755 
756             private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl;
757 
758             public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) {
759                 mGetValueImpl = getValueImpl;
760             }
761 
762             @Override
763             public boolean disabledRwExported() {
764                 return getValue(Flags.FLAG_DISABLED_RW_EXPORTED,
765                     FeatureFlags::disabledRwExported);
766             }
767             @Override
768             public boolean enabledFixedRoExported() {
769                 return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED,
770                     FeatureFlags::enabledFixedRoExported);
771             }
772             @Override
773             public boolean enabledRoExported() {
774                 return getValue(Flags.FLAG_ENABLED_RO_EXPORTED,
775                     FeatureFlags::enabledRoExported);
776             }
777 
778             protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) {
779                 return mGetValueImpl.test(flagName, getter);
780             }
781 
782             public List<String> getFlagNames() {
783                 return Arrays.asList(
784                     Flags.FLAG_DISABLED_RW_EXPORTED,
785                     Flags.FLAG_ENABLED_FIXED_RO_EXPORTED,
786                     Flags.FLAG_ENABLED_RO_EXPORTED
787                 );
788             }
789 
790             private Set<String> mReadOnlyFlagsSet = new HashSet<>(
791                 Arrays.asList(
792                     ""
793                 )
794             );
795         }
796     "#;
797 
798         let mut file_set = HashMap::from([
799             ("com/android/aconfig/test/Flags.java", expect_flags_content),
800             ("com/android/aconfig/test/FeatureFlags.java", expect_feature_flags_content),
801             ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_feature_flags_impl_content),
802             (
803                 "com/android/aconfig/test/CustomFeatureFlags.java",
804                 expect_custom_feature_flags_content,
805             ),
806             (
807                 "com/android/aconfig/test/FakeFeatureFlagsImpl.java",
808                 EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT,
809             ),
810         ]);
811 
812         for file in generated_files {
813             let file_path = file.path.to_str().unwrap();
814             assert!(file_set.contains_key(file_path), "Cannot find {}", file_path);
815             assert_eq!(
816                 None,
817                 crate::test::first_significant_code_diff(
818                     file_set.get(file_path).unwrap(),
819                     &String::from_utf8(file.contents).unwrap()
820                 ),
821                 "File {} content is not correct",
822                 file_path
823             );
824             file_set.remove(file_path);
825         }
826 
827         assert!(file_set.is_empty());
828     }
829 
830     #[test]
test_generate_java_code_test()831     fn test_generate_java_code_test() {
832         let parsed_flags = crate::test::parse_test_flags();
833         let mode = CodegenMode::Test;
834         let modified_parsed_flags =
835             crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
836         let generated_files =
837             generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
838                 .unwrap();
839 
840         let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
841             + r#"
842             public static void setFeatureFlags(FeatureFlags featureFlags) {
843                 Flags.FEATURE_FLAGS = featureFlags;
844             }
845             public static void unsetFeatureFlags() {
846                 Flags.FEATURE_FLAGS = null;
847             }
848             private static FeatureFlags FEATURE_FLAGS;
849         }
850         "#;
851         let expect_featureflagsimpl_content = r#"
852         package com.android.aconfig.test;
853         // TODO(b/303773055): Remove the annotation after access issue is resolved.
854         import android.compat.annotation.UnsupportedAppUsage;
855         /** @hide */
856         public final class FeatureFlagsImpl implements FeatureFlags {
857             @Override
858             @com.android.aconfig.annotations.AconfigFlagAccessor
859             @UnsupportedAppUsage
860             public boolean disabledRo() {
861                 throw new UnsupportedOperationException(
862                     "Method is not implemented.");
863             }
864             @Override
865             @com.android.aconfig.annotations.AconfigFlagAccessor
866             @UnsupportedAppUsage
867             public boolean disabledRw() {
868                 throw new UnsupportedOperationException(
869                     "Method is not implemented.");
870             }
871             @Override
872             @com.android.aconfig.annotations.AconfigFlagAccessor
873             @UnsupportedAppUsage
874             public boolean disabledRwExported() {
875                 throw new UnsupportedOperationException(
876                     "Method is not implemented.");
877             }
878             @Override
879             @com.android.aconfig.annotations.AconfigFlagAccessor
880             @UnsupportedAppUsage
881             public boolean disabledRwInOtherNamespace() {
882                 throw new UnsupportedOperationException(
883                     "Method is not implemented.");
884             }
885             @Override
886             @com.android.aconfig.annotations.AconfigFlagAccessor
887             @UnsupportedAppUsage
888             public boolean enabledFixedRo() {
889                 throw new UnsupportedOperationException(
890                     "Method is not implemented.");
891             }
892             @Override
893             @com.android.aconfig.annotations.AconfigFlagAccessor
894             @UnsupportedAppUsage
895             public boolean enabledFixedRoExported() {
896                 throw new UnsupportedOperationException(
897                     "Method is not implemented.");
898             }
899             @Override
900             @com.android.aconfig.annotations.AconfigFlagAccessor
901             @UnsupportedAppUsage
902             public boolean enabledRo() {
903                 throw new UnsupportedOperationException(
904                     "Method is not implemented.");
905             }
906             @Override
907             @com.android.aconfig.annotations.AconfigFlagAccessor
908             @UnsupportedAppUsage
909             public boolean enabledRoExported() {
910                 throw new UnsupportedOperationException(
911                     "Method is not implemented.");
912             }
913             @Override
914             @com.android.aconfig.annotations.AconfigFlagAccessor
915             @UnsupportedAppUsage
916             public boolean enabledRw() {
917                 throw new UnsupportedOperationException(
918                     "Method is not implemented.");
919             }
920         }
921         "#;
922 
923         let mut file_set = HashMap::from([
924             ("com/android/aconfig/test/Flags.java", expect_flags_content.as_str()),
925             ("com/android/aconfig/test/FeatureFlags.java", EXPECTED_FEATUREFLAGS_COMMON_CONTENT),
926             ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content),
927             (
928                 "com/android/aconfig/test/CustomFeatureFlags.java",
929                 EXPECTED_CUSTOMFEATUREFLAGS_CONTENT,
930             ),
931             (
932                 "com/android/aconfig/test/FakeFeatureFlagsImpl.java",
933                 EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT,
934             ),
935         ]);
936 
937         for file in generated_files {
938             let file_path = file.path.to_str().unwrap();
939             assert!(file_set.contains_key(file_path), "Cannot find {}", file_path);
940             assert_eq!(
941                 None,
942                 crate::test::first_significant_code_diff(
943                     file_set.get(file_path).unwrap(),
944                     &String::from_utf8(file.contents).unwrap()
945                 ),
946                 "File {} content is not correct",
947                 file_path
948             );
949             file_set.remove(file_path);
950         }
951 
952         assert!(file_set.is_empty());
953     }
954 
955     #[test]
test_generate_java_code_force_read_only()956     fn test_generate_java_code_force_read_only() {
957         let parsed_flags = crate::test::parse_test_flags();
958         let mode = CodegenMode::ForceReadOnly;
959         let modified_parsed_flags =
960             crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
961         let generated_files =
962             generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
963                 .unwrap();
964         let expect_featureflags_content = r#"
965         package com.android.aconfig.test;
966         // TODO(b/303773055): Remove the annotation after access issue is resolved.
967         import android.compat.annotation.UnsupportedAppUsage;
968         /** @hide */
969         public interface FeatureFlags {
970             @com.android.aconfig.annotations.AssumeFalseForR8
971             @com.android.aconfig.annotations.AconfigFlagAccessor
972             @UnsupportedAppUsage
973             boolean disabledRo();
974             @com.android.aconfig.annotations.AssumeFalseForR8
975             @com.android.aconfig.annotations.AconfigFlagAccessor
976             @UnsupportedAppUsage
977             boolean disabledRw();
978             @com.android.aconfig.annotations.AssumeFalseForR8
979             @com.android.aconfig.annotations.AconfigFlagAccessor
980             @UnsupportedAppUsage
981             boolean disabledRwInOtherNamespace();
982             @com.android.aconfig.annotations.AssumeTrueForR8
983             @com.android.aconfig.annotations.AconfigFlagAccessor
984             @UnsupportedAppUsage
985             boolean enabledFixedRo();
986             @com.android.aconfig.annotations.AssumeTrueForR8
987             @com.android.aconfig.annotations.AconfigFlagAccessor
988             @UnsupportedAppUsage
989             boolean enabledRo();
990             @com.android.aconfig.annotations.AssumeTrueForR8
991             @com.android.aconfig.annotations.AconfigFlagAccessor
992             @UnsupportedAppUsage
993             boolean enabledRw();
994         }"#;
995 
996         let expect_featureflagsimpl_content = r#"
997         package com.android.aconfig.test;
998         // TODO(b/303773055): Remove the annotation after access issue is resolved.
999         import android.compat.annotation.UnsupportedAppUsage;
1000         /** @hide */
1001         public final class FeatureFlagsImpl implements FeatureFlags {
1002             @Override
1003             @com.android.aconfig.annotations.AconfigFlagAccessor
1004             @UnsupportedAppUsage
1005             public boolean disabledRo() {
1006                 return false;
1007             }
1008             @Override
1009             @com.android.aconfig.annotations.AconfigFlagAccessor
1010             @UnsupportedAppUsage
1011             public boolean disabledRw() {
1012                 return false;
1013             }
1014             @Override
1015             @com.android.aconfig.annotations.AconfigFlagAccessor
1016             @UnsupportedAppUsage
1017             public boolean disabledRwInOtherNamespace() {
1018                 return false;
1019             }
1020             @Override
1021             @com.android.aconfig.annotations.AconfigFlagAccessor
1022             @UnsupportedAppUsage
1023             public boolean enabledFixedRo() {
1024                 return true;
1025             }
1026             @Override
1027             @com.android.aconfig.annotations.AconfigFlagAccessor
1028             @UnsupportedAppUsage
1029             public boolean enabledRo() {
1030                 return true;
1031             }
1032             @Override
1033             @com.android.aconfig.annotations.AconfigFlagAccessor
1034             @UnsupportedAppUsage
1035             public boolean enabledRw() {
1036                 return true;
1037             }
1038         }
1039         "#;
1040 
1041         let expect_flags_content = r#"
1042         package com.android.aconfig.test;
1043         // TODO(b/303773055): Remove the annotation after access issue is resolved.
1044         import android.compat.annotation.UnsupportedAppUsage;
1045         /** @hide */
1046         public final class Flags {
1047             /** @hide */
1048             public static final String FLAG_DISABLED_RO = "com.android.aconfig.test.disabled_ro";
1049             /** @hide */
1050             public static final String FLAG_DISABLED_RW = "com.android.aconfig.test.disabled_rw";
1051             /** @hide */
1052             public static final String FLAG_DISABLED_RW_IN_OTHER_NAMESPACE = "com.android.aconfig.test.disabled_rw_in_other_namespace";
1053             /** @hide */
1054             public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro";
1055             /** @hide */
1056             public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro";
1057             /** @hide */
1058             public static final String FLAG_ENABLED_RW = "com.android.aconfig.test.enabled_rw";
1059             @com.android.aconfig.annotations.AssumeFalseForR8
1060             @com.android.aconfig.annotations.AconfigFlagAccessor
1061             @UnsupportedAppUsage
1062             public static boolean disabledRo() {
1063                 return FEATURE_FLAGS.disabledRo();
1064             }
1065             @com.android.aconfig.annotations.AssumeFalseForR8
1066             @com.android.aconfig.annotations.AconfigFlagAccessor
1067             @UnsupportedAppUsage
1068             public static boolean disabledRw() {
1069                 return FEATURE_FLAGS.disabledRw();
1070             }
1071             @com.android.aconfig.annotations.AssumeFalseForR8
1072             @com.android.aconfig.annotations.AconfigFlagAccessor
1073             @UnsupportedAppUsage
1074             public static boolean disabledRwInOtherNamespace() {
1075                 return FEATURE_FLAGS.disabledRwInOtherNamespace();
1076             }
1077             @com.android.aconfig.annotations.AssumeTrueForR8
1078             @com.android.aconfig.annotations.AconfigFlagAccessor
1079             @UnsupportedAppUsage
1080             public static boolean enabledFixedRo() {
1081                 return FEATURE_FLAGS.enabledFixedRo();
1082             }
1083             @com.android.aconfig.annotations.AssumeTrueForR8
1084             @com.android.aconfig.annotations.AconfigFlagAccessor
1085             @UnsupportedAppUsage
1086             public static boolean enabledRo() {
1087                 return FEATURE_FLAGS.enabledRo();
1088             }
1089             @com.android.aconfig.annotations.AssumeTrueForR8
1090             @com.android.aconfig.annotations.AconfigFlagAccessor
1091             @UnsupportedAppUsage
1092             public static boolean enabledRw() {
1093                 return FEATURE_FLAGS.enabledRw();
1094             }
1095             private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
1096         }"#;
1097 
1098         let expect_customfeatureflags_content = r#"
1099         package com.android.aconfig.test;
1100 
1101         // TODO(b/303773055): Remove the annotation after access issue is resolved.
1102         import android.compat.annotation.UnsupportedAppUsage;
1103         import java.util.Arrays;
1104         import java.util.HashSet;
1105         import java.util.List;
1106         import java.util.Set;
1107         import java.util.function.BiPredicate;
1108         import java.util.function.Predicate;
1109 
1110         /** @hide */
1111         public class CustomFeatureFlags implements FeatureFlags {
1112 
1113             private BiPredicate<String, Predicate<FeatureFlags>> mGetValueImpl;
1114 
1115             public CustomFeatureFlags(BiPredicate<String, Predicate<FeatureFlags>> getValueImpl) {
1116                 mGetValueImpl = getValueImpl;
1117             }
1118 
1119             @Override
1120             @UnsupportedAppUsage
1121             public boolean disabledRo() {
1122                 return getValue(Flags.FLAG_DISABLED_RO,
1123                         FeatureFlags::disabledRo);
1124             }
1125             @Override
1126             @UnsupportedAppUsage
1127             public boolean disabledRw() {
1128                 return getValue(Flags.FLAG_DISABLED_RW,
1129                     FeatureFlags::disabledRw);
1130             }
1131             @Override
1132             @UnsupportedAppUsage
1133             public boolean disabledRwInOtherNamespace() {
1134                 return getValue(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE,
1135                     FeatureFlags::disabledRwInOtherNamespace);
1136             }
1137             @Override
1138             @UnsupportedAppUsage
1139             public boolean enabledFixedRo() {
1140                 return getValue(Flags.FLAG_ENABLED_FIXED_RO,
1141                     FeatureFlags::enabledFixedRo);
1142             }
1143             @Override
1144             @UnsupportedAppUsage
1145             public boolean enabledRo() {
1146                 return getValue(Flags.FLAG_ENABLED_RO,
1147                     FeatureFlags::enabledRo);
1148             }
1149             @Override
1150             @UnsupportedAppUsage
1151             public boolean enabledRw() {
1152                 return getValue(Flags.FLAG_ENABLED_RW,
1153                     FeatureFlags::enabledRw);
1154             }
1155 
1156             public boolean isFlagReadOnlyOptimized(String flagName) {
1157                 if (mReadOnlyFlagsSet.contains(flagName) &&
1158                     isOptimizationEnabled()) {
1159                         return true;
1160                 }
1161                 return false;
1162             }
1163 
1164             @com.android.aconfig.annotations.AssumeTrueForR8
1165             private boolean isOptimizationEnabled() {
1166                 return false;
1167             }
1168 
1169             protected boolean getValue(String flagName, Predicate<FeatureFlags> getter) {
1170                 return mGetValueImpl.test(flagName, getter);
1171             }
1172 
1173             public List<String> getFlagNames() {
1174                 return Arrays.asList(
1175                     Flags.FLAG_DISABLED_RO,
1176                     Flags.FLAG_DISABLED_RW,
1177                     Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE,
1178                     Flags.FLAG_ENABLED_FIXED_RO,
1179                     Flags.FLAG_ENABLED_RO,
1180                     Flags.FLAG_ENABLED_RW
1181                 );
1182             }
1183 
1184             private Set<String> mReadOnlyFlagsSet = new HashSet<>(
1185                 Arrays.asList(
1186                     Flags.FLAG_DISABLED_RO,
1187                     Flags.FLAG_DISABLED_RW,
1188                     Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE,
1189                     Flags.FLAG_ENABLED_FIXED_RO,
1190                     Flags.FLAG_ENABLED_RO,
1191                     Flags.FLAG_ENABLED_RW,
1192                     ""
1193                 )
1194             );
1195         }
1196         "#;
1197 
1198         let mut file_set = HashMap::from([
1199             ("com/android/aconfig/test/Flags.java", expect_flags_content),
1200             ("com/android/aconfig/test/FeatureFlagsImpl.java", expect_featureflagsimpl_content),
1201             ("com/android/aconfig/test/FeatureFlags.java", expect_featureflags_content),
1202             ("com/android/aconfig/test/CustomFeatureFlags.java", expect_customfeatureflags_content),
1203             (
1204                 "com/android/aconfig/test/FakeFeatureFlagsImpl.java",
1205                 EXPECTED_FAKEFEATUREFLAGSIMPL_CONTENT,
1206             ),
1207         ]);
1208 
1209         for file in generated_files {
1210             let file_path = file.path.to_str().unwrap();
1211             assert!(file_set.contains_key(file_path), "Cannot find {}", file_path);
1212             assert_eq!(
1213                 None,
1214                 crate::test::first_significant_code_diff(
1215                     file_set.get(file_path).unwrap(),
1216                     &String::from_utf8(file.contents).unwrap()
1217                 ),
1218                 "File {} content is not correct",
1219                 file_path
1220             );
1221             file_set.remove(file_path);
1222         }
1223 
1224         assert!(file_set.is_empty());
1225     }
1226 
1227     #[test]
test_format_java_method_name()1228     fn test_format_java_method_name() {
1229         let expected = "someSnakeName";
1230         let input = "____some_snake___name____";
1231         let formatted_name = format_java_method_name(input);
1232         assert_eq!(expected, formatted_name);
1233 
1234         let input = "someSnakeName";
1235         let formatted_name = format_java_method_name(input);
1236         assert_eq!(expected, formatted_name);
1237 
1238         let input = "SomeSnakeName";
1239         let formatted_name = format_java_method_name(input);
1240         assert_eq!(expected, formatted_name);
1241 
1242         let input = "SomeSnakeName_";
1243         let formatted_name = format_java_method_name(input);
1244         assert_eq!(expected, formatted_name);
1245 
1246         let input = "_SomeSnakeName";
1247         let formatted_name = format_java_method_name(input);
1248         assert_eq!(expected, formatted_name);
1249     }
1250 
1251     #[test]
test_format_property_name()1252     fn test_format_property_name() {
1253         let expected = "mPropertiesSomeSnakeName";
1254         let input = "____some_snake___name____";
1255         let formatted_name = format_property_name(input);
1256         assert_eq!(expected, formatted_name);
1257 
1258         let input = "someSnakeName";
1259         let formatted_name = format_property_name(input);
1260         assert_eq!(expected, formatted_name);
1261 
1262         let input = "SomeSnakeName";
1263         let formatted_name = format_property_name(input);
1264         assert_eq!(expected, formatted_name);
1265 
1266         let input = "SomeSnakeName_";
1267         let formatted_name = format_property_name(input);
1268         assert_eq!(expected, formatted_name);
1269     }
1270 }
1271