| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| //! `aconfig_protos` is a crate for the protos defined for aconfig |
| // When building with the Android tool-chain |
| // |
| // - an external crate `aconfig_protos` will be generated |
| // - the feature "cargo" will be disabled |
| // |
| // When building with cargo |
| // |
| // - a local sub-module will be generated in OUT_DIR and included in this file |
| // - the feature "cargo" will be enabled |
| // |
| // This module hides these differences from the rest of aconfig. |
| |
| // ---- When building with the Android tool-chain ---- |
| #[cfg(not(feature = "cargo"))] |
| mod auto_generated { |
| pub use aconfig_rust_proto::aconfig::flag_metadata::Flag_purpose as ProtoFlagPurpose; |
| pub use aconfig_rust_proto::aconfig::Flag_declaration as ProtoFlagDeclaration; |
| pub use aconfig_rust_proto::aconfig::Flag_declarations as ProtoFlagDeclarations; |
| pub use aconfig_rust_proto::aconfig::Flag_metadata as ProtoFlagMetadata; |
| pub use aconfig_rust_proto::aconfig::Flag_permission as ProtoFlagPermission; |
| pub use aconfig_rust_proto::aconfig::Flag_state as ProtoFlagState; |
| pub use aconfig_rust_proto::aconfig::Flag_value as ProtoFlagValue; |
| pub use aconfig_rust_proto::aconfig::Flag_values as ProtoFlagValues; |
| pub use aconfig_rust_proto::aconfig::Parsed_flag as ProtoParsedFlag; |
| pub use aconfig_rust_proto::aconfig::Parsed_flags as ProtoParsedFlags; |
| pub use aconfig_rust_proto::aconfig::Tracepoint as ProtoTracepoint; |
| } |
| |
| // ---- When building with cargo ---- |
| #[cfg(feature = "cargo")] |
| mod auto_generated { |
| // include! statements should be avoided (because they import file contents verbatim), but |
| // because this is only used during local development, and only if using cargo instead of the |
| // Android tool-chain, we allow it |
| include!(concat!(env!("OUT_DIR"), "/aconfig_proto/mod.rs")); |
| pub use aconfig::flag_metadata::Flag_purpose as ProtoFlagPurpose; |
| pub use aconfig::Flag_declaration as ProtoFlagDeclaration; |
| pub use aconfig::Flag_declarations as ProtoFlagDeclarations; |
| pub use aconfig::Flag_metadata as ProtoFlagMetadata; |
| pub use aconfig::Flag_permission as ProtoFlagPermission; |
| pub use aconfig::Flag_state as ProtoFlagState; |
| pub use aconfig::Flag_value as ProtoFlagValue; |
| pub use aconfig::Flag_values as ProtoFlagValues; |
| pub use aconfig::Parsed_flag as ProtoParsedFlag; |
| pub use aconfig::Parsed_flags as ProtoParsedFlags; |
| pub use aconfig::Tracepoint as ProtoTracepoint; |
| } |
| |
| // ---- Common for both the Android tool-chain and cargo ---- |
| pub use auto_generated::*; |
| |
| use anyhow::Result; |
| use paste::paste; |
| |
| /// Path to proto file |
| const ACONFIG_PROTO_PATH: &str = "//build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto"; |
| |
| /// Check if the name identifier is valid |
| pub fn is_valid_name_ident(s: &str) -> bool { |
| // Identifiers must match [a-z][a-z0-9_]*, except consecutive underscores are not allowed |
| if s.contains("__") { |
| return false; |
| } |
| let mut chars = s.chars(); |
| let Some(first) = chars.next() else { |
| return false; |
| }; |
| if !first.is_ascii_lowercase() { |
| return false; |
| } |
| chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_') |
| } |
| |
| /// Check if the package identifier is valid |
| pub fn is_valid_package_ident(s: &str) -> bool { |
| if !s.contains('.') { |
| return false; |
| } |
| s.split('.').all(is_valid_name_ident) |
| } |
| |
| /// Check if the container identifier is valid |
| pub fn is_valid_container_ident(s: &str) -> bool { |
| s.split('.').all(is_valid_name_ident) |
| } |
| |
| fn try_from_text_proto<T>(s: &str) -> Result<T> |
| where |
| T: protobuf::MessageFull, |
| { |
| protobuf::text_format::parse_from_str(s).map_err(|e| e.into()) |
| } |
| |
| macro_rules! ensure_required_fields { |
| ($type:expr, $struct:expr, $($field:expr),+) => { |
| $( |
| paste! { |
| ensure!($struct.[<has_ $field>](), "bad {}: missing {}", $type, $field); |
| } |
| )+ |
| }; |
| } |
| |
| /// Utility module for flag_declaration proto |
| pub mod flag_declaration { |
| use super::*; |
| use anyhow::ensure; |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(pdf: &ProtoFlagDeclaration) -> Result<()> { |
| ensure_required_fields!("flag declaration", pdf, "name", "namespace", "description"); |
| |
| ensure!( |
| is_valid_name_ident(pdf.name()), |
| "bad flag declaration: bad name {} expected snake_case string; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pdf.name() |
| ); |
| ensure!( |
| is_valid_name_ident(pdf.namespace()), |
| "bad flag declaration: bad namespace {} expected snake_case string; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pdf.namespace() |
| ); |
| ensure!(!pdf.description().is_empty(), "bad flag declaration: empty description"); |
| ensure!(pdf.bug.len() == 1, "bad flag declaration: exactly one bug required"); |
| |
| Ok(()) |
| } |
| } |
| |
| /// Utility module for flag_declarations proto |
| pub mod flag_declarations { |
| use super::*; |
| use anyhow::ensure; |
| |
| /// Construct a proto instance from a textproto string content |
| pub fn try_from_text_proto(s: &str) -> Result<ProtoFlagDeclarations> { |
| let pdf: ProtoFlagDeclarations = super::try_from_text_proto(s)?; |
| verify_fields(&pdf)?; |
| Ok(pdf) |
| } |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(pdf: &ProtoFlagDeclarations) -> Result<()> { |
| ensure_required_fields!("flag declarations", pdf, "package"); |
| // TODO(b/312769710): Make the container field required. |
| ensure!( |
| is_valid_package_ident(pdf.package()), |
| "bad flag declarations: bad package {} expected snake_case strings delimited by dots; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pdf.package() |
| ); |
| ensure!( |
| !pdf.has_container() || is_valid_container_ident(pdf.container()), |
| "bad flag declarations: bad container" |
| ); |
| for flag_declaration in pdf.flag.iter() { |
| super::flag_declaration::verify_fields(flag_declaration)?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| /// Utility module for flag_value proto |
| pub mod flag_value { |
| use super::*; |
| use anyhow::ensure; |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(fv: &ProtoFlagValue) -> Result<()> { |
| ensure_required_fields!("flag value", fv, "package", "name", "state", "permission"); |
| |
| ensure!( |
| is_valid_package_ident(fv.package()), |
| "bad flag value: bad package {} expected snake_case strings delimited by dots; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| fv.package() |
| ); |
| ensure!( |
| is_valid_name_ident(fv.name()), |
| "bad flag value: bad name {} expected snake_case string; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| fv.name() |
| ); |
| |
| Ok(()) |
| } |
| } |
| |
| /// Utility module for flag_values proto |
| pub mod flag_values { |
| use super::*; |
| |
| /// Construct a proto instance from a textproto string content |
| pub fn try_from_text_proto(s: &str) -> Result<ProtoFlagValues> { |
| let pfv: ProtoFlagValues = super::try_from_text_proto(s)?; |
| verify_fields(&pfv)?; |
| Ok(pfv) |
| } |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(pfv: &ProtoFlagValues) -> Result<()> { |
| for flag_value in pfv.flag_value.iter() { |
| super::flag_value::verify_fields(flag_value)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// Utility module for flag_permission proto enum |
| pub mod flag_permission { |
| use super::*; |
| use anyhow::bail; |
| |
| /// Construct a flag permission proto enum from string |
| pub fn parse_from_str(permission: &str) -> Result<ProtoFlagPermission> { |
| match permission.to_ascii_lowercase().as_str() { |
| "read_write" => Ok(ProtoFlagPermission::READ_WRITE), |
| "read_only" => Ok(ProtoFlagPermission::READ_ONLY), |
| _ => bail!("Permission needs to be read_only or read_write."), |
| } |
| } |
| |
| /// Serialize flag permission proto enum to string |
| pub fn to_string(permission: &ProtoFlagPermission) -> &str { |
| match permission { |
| ProtoFlagPermission::READ_WRITE => "read_write", |
| ProtoFlagPermission::READ_ONLY => "read_only", |
| } |
| } |
| } |
| |
| /// Utility module for tracepoint proto |
| pub mod tracepoint { |
| use super::*; |
| use anyhow::ensure; |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(tp: &ProtoTracepoint) -> Result<()> { |
| ensure_required_fields!("tracepoint", tp, "source", "state", "permission"); |
| |
| ensure!(!tp.source().is_empty(), "bad tracepoint: empty source"); |
| |
| Ok(()) |
| } |
| } |
| |
| /// Utility module for parsed_flag proto |
| pub mod parsed_flag { |
| use super::*; |
| use anyhow::ensure; |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(pf: &ProtoParsedFlag) -> Result<()> { |
| ensure_required_fields!( |
| "parsed flag", |
| pf, |
| "package", |
| "name", |
| "namespace", |
| "description", |
| "state", |
| "permission" |
| ); |
| |
| ensure!( |
| is_valid_package_ident(pf.package()), |
| "bad parsed flag: bad package {} expected snake_case strings delimited by dots; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pf.package() |
| ); |
| ensure!( |
| !pf.has_container() || is_valid_container_ident(pf.container()), |
| "bad parsed flag: bad container" |
| ); |
| ensure!( |
| is_valid_name_ident(pf.name()), |
| "bad parsed flag: bad name {} expected snake_case string; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pf.name() |
| ); |
| ensure!( |
| is_valid_name_ident(pf.namespace()), |
| "bad parsed flag: bad namespace {} expected snake_case string; \ |
| see {ACONFIG_PROTO_PATH} for details", |
| pf.namespace() |
| ); |
| ensure!(!pf.description().is_empty(), "bad parsed flag: empty description"); |
| ensure!(!pf.trace.is_empty(), "bad parsed flag: empty trace"); |
| for tp in pf.trace.iter() { |
| super::tracepoint::verify_fields(tp)?; |
| } |
| ensure!(pf.bug.len() == 1, "bad flag declaration: exactly one bug required"); |
| if pf.is_fixed_read_only() { |
| ensure!( |
| pf.permission() == ProtoFlagPermission::READ_ONLY, |
| "bad parsed flag: flag is is_fixed_read_only but permission is not READ_ONLY" |
| ); |
| for tp in pf.trace.iter() { |
| ensure!(tp.permission() == ProtoFlagPermission::READ_ONLY, |
| "bad parsed flag: flag is is_fixed_read_only but a tracepoint's permission is not READ_ONLY" |
| ); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Get the file path of the corresponding flag declaration |
| pub fn path_to_declaration(pf: &ProtoParsedFlag) -> &str { |
| debug_assert!(!pf.trace.is_empty()); |
| pf.trace[0].source() |
| } |
| } |
| |
| /// Utility module for parsed_flags proto |
| pub mod parsed_flags { |
| use super::*; |
| use anyhow::bail; |
| use std::cmp::Ordering; |
| |
| /// Construct a proto instance from a binary proto bytes |
| pub fn try_from_binary_proto(bytes: &[u8]) -> Result<ProtoParsedFlags> { |
| let message: ProtoParsedFlags = protobuf::Message::parse_from_bytes(bytes)?; |
| verify_fields(&message)?; |
| Ok(message) |
| } |
| |
| /// Ensure the proto instance is valid by checking its fields |
| pub fn verify_fields(pf: &ProtoParsedFlags) -> Result<()> { |
| use crate::parsed_flag::path_to_declaration; |
| |
| let mut previous: Option<&ProtoParsedFlag> = None; |
| for parsed_flag in pf.parsed_flag.iter() { |
| if let Some(prev) = previous { |
| let a = create_sorting_key(prev); |
| let b = create_sorting_key(parsed_flag); |
| match a.cmp(&b) { |
| Ordering::Less => {} |
| Ordering::Equal => bail!( |
| "bad parsed flags: duplicate flag {} (defined in {} and {})", |
| a, |
| path_to_declaration(prev), |
| path_to_declaration(parsed_flag) |
| ), |
| Ordering::Greater => { |
| bail!("bad parsed flags: not sorted: {} comes before {}", a, b) |
| } |
| } |
| } |
| super::parsed_flag::verify_fields(parsed_flag)?; |
| previous = Some(parsed_flag); |
| } |
| Ok(()) |
| } |
| |
| /// Merge multipe parsed_flags proto |
| pub fn merge(parsed_flags: Vec<ProtoParsedFlags>, dedup: bool) -> Result<ProtoParsedFlags> { |
| let mut merged = ProtoParsedFlags::new(); |
| for mut pfs in parsed_flags.into_iter() { |
| merged.parsed_flag.append(&mut pfs.parsed_flag); |
| } |
| merged.parsed_flag.sort_by_cached_key(create_sorting_key); |
| if dedup { |
| // Deduplicate identical protobuf messages. Messages with the same sorting key but |
| // different fields (including the path to the original source file) will not be |
| // deduplicated and trigger an error in verify_fields. |
| merged.parsed_flag.dedup(); |
| } |
| verify_fields(&merged)?; |
| Ok(merged) |
| } |
| |
| /// Sort parsed flags |
| pub fn sort_parsed_flags(pf: &mut ProtoParsedFlags) { |
| pf.parsed_flag.sort_by_key(create_sorting_key); |
| } |
| |
| fn create_sorting_key(pf: &ProtoParsedFlag) -> String { |
| pf.fully_qualified_name() |
| } |
| } |
| |
| /// ParsedFlagExt trait |
| pub trait ParsedFlagExt { |
| /// Return the fully qualified name |
| fn fully_qualified_name(&self) -> String; |
| } |
| |
| impl ParsedFlagExt for ProtoParsedFlag { |
| fn fully_qualified_name(&self) -> String { |
| format!("{}.{}", self.package(), self.name()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_flag_declarations_try_from_text_proto() { |
| // valid input |
| let flag_declarations = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "system" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "123" |
| is_exported: true |
| } |
| flag { |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "abc" |
| is_fixed_read_only: true |
| } |
| "#, |
| ) |
| .unwrap(); |
| assert_eq!(flag_declarations.package(), "com.foo.bar"); |
| assert_eq!(flag_declarations.container(), "system"); |
| let first = flag_declarations.flag.iter().find(|pf| pf.name() == "first").unwrap(); |
| assert_eq!(first.name(), "first"); |
| assert_eq!(first.namespace(), "first_ns"); |
| assert_eq!(first.description(), "This is the description of the first flag."); |
| assert_eq!(first.bug, vec!["123"]); |
| assert!(!first.is_fixed_read_only()); |
| assert!(first.is_exported()); |
| let second = flag_declarations.flag.iter().find(|pf| pf.name() == "second").unwrap(); |
| assert_eq!(second.name(), "second"); |
| assert_eq!(second.namespace(), "second_ns"); |
| assert_eq!(second.description(), "This is the description of the second flag."); |
| assert_eq!(second.bug, vec!["abc"]); |
| assert!(second.is_fixed_read_only()); |
| assert!(!second.is_exported()); |
| |
| // valid input: missing container in flag declarations is supported |
| let flag_declarations = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "123" |
| } |
| "#, |
| ) |
| .unwrap(); |
| assert_eq!(flag_declarations.container(), ""); |
| assert!(!flag_declarations.has_container()); |
| |
| // bad input: missing package in flag declarations |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| container: "system" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| } |
| flag { |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad flag declarations: missing package"); |
| |
| // bad input: missing namespace in flag declaration |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "system" |
| flag { |
| name: "first" |
| description: "This is the description of the first flag." |
| } |
| flag { |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad flag declaration: missing namespace"); |
| |
| // bad input: bad package name in flag declarations |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "_com.FOO__BAR" |
| container: "system" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| } |
| flag { |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag declarations: bad package")); |
| |
| // bad input: bad name in flag declaration |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "system" |
| flag { |
| name: "FIRST" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| } |
| flag { |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag declaration: bad name")); |
| |
| // bad input: no bug entries in flag declaration |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "system" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag declaration: exactly one bug required")); |
| |
| // bad input: multiple bug entries in flag declaration |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "system" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "123" |
| bug: "abc" |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag declaration: exactly one bug required")); |
| |
| // bad input: invalid container name in flag declaration |
| let error = flag_declarations::try_from_text_proto( |
| r#" |
| package: "com.foo.bar" |
| container: "__bad_bad_container.com" |
| flag { |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "123" |
| bug: "abc" |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag declarations: bad container")); |
| |
| // TODO(b/312769710): Verify error when container is missing. |
| } |
| |
| #[test] |
| fn test_flag_values_try_from_text_proto() { |
| // valid input |
| let flag_values = flag_values::try_from_text_proto( |
| r#" |
| flag_value { |
| package: "com.first" |
| name: "first" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| flag_value { |
| package: "com.second" |
| name: "second" |
| state: ENABLED |
| permission: READ_WRITE |
| } |
| "#, |
| ) |
| .unwrap(); |
| let first = flag_values.flag_value.iter().find(|fv| fv.name() == "first").unwrap(); |
| assert_eq!(first.package(), "com.first"); |
| assert_eq!(first.name(), "first"); |
| assert_eq!(first.state(), ProtoFlagState::DISABLED); |
| assert_eq!(first.permission(), ProtoFlagPermission::READ_ONLY); |
| let second = flag_values.flag_value.iter().find(|fv| fv.name() == "second").unwrap(); |
| assert_eq!(second.package(), "com.second"); |
| assert_eq!(second.name(), "second"); |
| assert_eq!(second.state(), ProtoFlagState::ENABLED); |
| assert_eq!(second.permission(), ProtoFlagPermission::READ_WRITE); |
| |
| // bad input: bad package in flag value |
| let error = flag_values::try_from_text_proto( |
| r#" |
| flag_value { |
| package: "COM.FIRST" |
| name: "first" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag value: bad package")); |
| |
| // bad input: bad name in flag value |
| let error = flag_values::try_from_text_proto( |
| r#" |
| flag_value { |
| package: "com.first" |
| name: "FIRST" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert!(format!("{:?}", error).contains("bad flag value: bad name")); |
| |
| // bad input: missing state in flag value |
| let error = flag_values::try_from_text_proto( |
| r#" |
| flag_value { |
| package: "com.first" |
| name: "first" |
| permission: READ_ONLY |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad flag value: missing state"); |
| |
| // bad input: missing permission in flag value |
| let error = flag_values::try_from_text_proto( |
| r#" |
| flag_value { |
| package: "com.first" |
| name: "first" |
| state: DISABLED |
| } |
| "#, |
| ) |
| .unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad flag value: missing permission"); |
| } |
| |
| fn try_from_binary_proto_from_text_proto(text_proto: &str) -> Result<ProtoParsedFlags> { |
| use protobuf::Message; |
| |
| let parsed_flags: ProtoParsedFlags = try_from_text_proto(text_proto)?; |
| let mut binary_proto = Vec::new(); |
| parsed_flags.write_to_vec(&mut binary_proto)?; |
| parsed_flags::try_from_binary_proto(&binary_proto) |
| } |
| |
| #[test] |
| fn test_parsed_flags_try_from_text_proto() { |
| // valid input |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.first" |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "SOME_BUG" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| parsed_flag { |
| package: "com.second" |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "SOME_BUG" |
| state: ENABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| trace { |
| source: "flags.values" |
| state: ENABLED |
| permission: READ_ONLY |
| } |
| is_fixed_read_only: true |
| container: "system" |
| } |
| "#; |
| let parsed_flags = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| assert_eq!(parsed_flags.parsed_flag.len(), 2); |
| let second = parsed_flags.parsed_flag.iter().find(|fv| fv.name() == "second").unwrap(); |
| assert_eq!(second.package(), "com.second"); |
| assert_eq!(second.name(), "second"); |
| assert_eq!(second.namespace(), "second_ns"); |
| assert_eq!(second.description(), "This is the description of the second flag."); |
| assert_eq!(second.bug, vec!["SOME_BUG"]); |
| assert_eq!(second.state(), ProtoFlagState::ENABLED); |
| assert_eq!(second.permission(), ProtoFlagPermission::READ_ONLY); |
| assert_eq!(2, second.trace.len()); |
| assert_eq!(second.trace[0].source(), "flags.declarations"); |
| assert_eq!(second.trace[0].state(), ProtoFlagState::DISABLED); |
| assert_eq!(second.trace[0].permission(), ProtoFlagPermission::READ_ONLY); |
| assert_eq!(second.trace[1].source(), "flags.values"); |
| assert_eq!(second.trace[1].state(), ProtoFlagState::ENABLED); |
| assert_eq!(second.trace[1].permission(), ProtoFlagPermission::READ_ONLY); |
| assert!(second.is_fixed_read_only()); |
| |
| // valid input: empty |
| let parsed_flags = try_from_binary_proto_from_text_proto("").unwrap(); |
| assert!(parsed_flags.parsed_flag.is_empty()); |
| |
| // bad input: empty trace |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.first" |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| state: DISABLED |
| permission: READ_ONLY |
| container: "system" |
| } |
| "#; |
| let error = try_from_binary_proto_from_text_proto(text_proto).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flag: empty trace"); |
| |
| // bad input: missing namespace in parsed_flag |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.first" |
| name: "first" |
| description: "This is the description of the first flag." |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let error = try_from_binary_proto_from_text_proto(text_proto).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flag: missing namespace"); |
| |
| // bad input: parsed_flag not sorted by package |
| let text_proto = r#" |
| parsed_flag { |
| package: "bbb.bbb" |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| parsed_flag { |
| package: "aaa.aaa" |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "" |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let error = try_from_binary_proto_from_text_proto(text_proto).unwrap_err(); |
| assert_eq!( |
| format!("{:?}", error), |
| "bad parsed flags: not sorted: bbb.bbb.first comes before aaa.aaa.second" |
| ); |
| |
| // bad input: parsed_flag not sorted by name |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.foo" |
| name: "bbb" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| parsed_flag { |
| package: "com.foo" |
| name: "aaa" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "" |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let error = try_from_binary_proto_from_text_proto(text_proto).unwrap_err(); |
| assert_eq!( |
| format!("{:?}", error), |
| "bad parsed flags: not sorted: com.foo.bbb comes before com.foo.aaa" |
| ); |
| |
| // bad input: duplicate flags |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.foo" |
| name: "bar" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| parsed_flag { |
| package: "com.foo" |
| name: "bar" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "" |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let error = try_from_binary_proto_from_text_proto(text_proto).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flags: duplicate flag com.foo.bar (defined in flags.declarations and flags.declarations)"); |
| } |
| |
| #[test] |
| fn test_parsed_flag_path_to_declaration() { |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.foo" |
| name: "bar" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "b/12345678" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| trace { |
| source: "flags.values" |
| state: ENABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let parsed_flags = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| let parsed_flag = &parsed_flags.parsed_flag[0]; |
| assert_eq!(crate::parsed_flag::path_to_declaration(parsed_flag), "flags.declarations"); |
| } |
| |
| #[test] |
| fn test_parsed_flags_merge() { |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.first" |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "a" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| parsed_flag { |
| package: "com.second" |
| name: "second" |
| namespace: "second_ns" |
| description: "This is the description of the second flag." |
| bug: "b" |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let expected = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.first" |
| name: "first" |
| namespace: "first_ns" |
| description: "This is the description of the first flag." |
| bug: "a" |
| state: DISABLED |
| permission: READ_ONLY |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let first = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.second" |
| name: "second" |
| namespace: "second_ns" |
| bug: "b" |
| description: "This is the description of the second flag." |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| container: "system" |
| } |
| "#; |
| let second = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| |
| let text_proto = r#" |
| parsed_flag { |
| package: "com.second" |
| name: "second" |
| namespace: "second_ns" |
| bug: "b" |
| description: "This is the description of the second flag." |
| state: ENABLED |
| permission: READ_WRITE |
| trace { |
| source: "duplicate/flags.declarations" |
| state: DISABLED |
| permission: READ_ONLY |
| } |
| } |
| "#; |
| let second_duplicate = try_from_binary_proto_from_text_proto(text_proto).unwrap(); |
| |
| // bad cases |
| |
| // two of the same flag with dedup disabled |
| let error = parsed_flags::merge(vec![first.clone(), first.clone()], false).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flags: duplicate flag com.first.first (defined in flags.declarations and flags.declarations)"); |
| |
| // two conflicting flags with dedup disabled |
| let error = |
| parsed_flags::merge(vec![second.clone(), second_duplicate.clone()], false).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flags: duplicate flag com.second.second (defined in flags.declarations and duplicate/flags.declarations)"); |
| |
| // two conflicting flags with dedup enabled |
| let error = |
| parsed_flags::merge(vec![second.clone(), second_duplicate.clone()], true).unwrap_err(); |
| assert_eq!(format!("{:?}", error), "bad parsed flags: duplicate flag com.second.second (defined in flags.declarations and duplicate/flags.declarations)"); |
| |
| // valid cases |
| assert!(parsed_flags::merge(vec![], false).unwrap().parsed_flag.is_empty()); |
| assert!(parsed_flags::merge(vec![], true).unwrap().parsed_flag.is_empty()); |
| assert_eq!(first, parsed_flags::merge(vec![first.clone()], false).unwrap()); |
| assert_eq!(first, parsed_flags::merge(vec![first.clone()], true).unwrap()); |
| assert_eq!( |
| expected, |
| parsed_flags::merge(vec![first.clone(), second.clone()], false).unwrap() |
| ); |
| assert_eq!( |
| expected, |
| parsed_flags::merge(vec![first.clone(), second.clone()], true).unwrap() |
| ); |
| assert_eq!( |
| expected, |
| parsed_flags::merge(vec![second.clone(), first.clone()], false).unwrap() |
| ); |
| assert_eq!( |
| expected, |
| parsed_flags::merge(vec![second.clone(), first.clone()], true).unwrap() |
| ); |
| |
| // two identical flags with dedup enabled |
| assert_eq!(first, parsed_flags::merge(vec![first.clone(), first.clone()], true).unwrap()); |
| } |
| } |