blob: 58c546ddd5b98008880d58f2da9012302b57fa68 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::error::ComponentError, fidl_fuchsia_data as fdata, selectors::VerboseError,
std::collections::BTreeMap,
};
#[derive(Debug)]
pub(crate) struct ProgramSpec {
/// The name of the accessor to use.
pub accessor: Accessor,
/// The maximum amount of time to wait for this value to be present.
pub timeout_seconds: i64,
/// The test cases to execute.
pub cases: BTreeMap<String, TestCase>,
}
impl ProgramSpec {
/// Returns the list of test names from the program spec.
pub fn test_names(&self) -> Vec<String> {
self.cases.iter().map(|c| c.0.to_string()).collect()
}
}
impl TryFrom<fdata::Dictionary> for ProgramSpec {
type Error = ComponentError;
fn try_from(dict: fdata::Dictionary) -> Result<Self, Self::Error> {
let mut accessor = None;
let mut timeout_seconds = None;
let mut cases = None;
for case in dict.entries.ok_or(ComponentError::MissingRequiredKey("program"))?.into_iter() {
match case.key.as_ref() {
"accessor" => {
accessor = match *case
.value
.ok_or(ComponentError::MissingRequiredKey("accessor"))?
{
fdata::DictionaryValue::Str(s) if s == "ALL" => Some(Accessor::All),
fdata::DictionaryValue::Str(s) if s == "FEEDBACK" => {
Some(Accessor::Feedback)
}
fdata::DictionaryValue::Str(s) if s == "LEGACY" => Some(Accessor::Legacy),
v => {
return Err(ComponentError::UnknownAccessorValue(format!("{:?}", v)));
}
}
}
"timeout_seconds" => {
if let fdata::DictionaryValue::Str(val) =
*case.value.ok_or(ComponentError::MissingRequiredKey("timeout_seconds"))?
{
timeout_seconds = Some(val.parse::<i64>().map_err(|_| {
ComponentError::WrongType("timeout_seconds", "numeric string")
})?);
if *timeout_seconds.as_ref().unwrap() <= 0 {
return Err(ComponentError::InvalidTimeout);
}
} else {
return Err(ComponentError::WrongType("timeout_seconds", "numeric string"));
}
}
"cases" => {
if let fdata::DictionaryValue::StrVec(val) =
*case.value.ok_or(ComponentError::MissingRequiredKey("cases"))?
{
let items: Result<Vec<TestCase>, _> =
val.into_iter().map(|v| TestCase::try_from(v)).collect();
cases = Some(items?);
} else {
return Err(ComponentError::WrongType("cases", "string array"));
}
}
k => {
return Err(ComponentError::UnknownKey(k.to_string()));
}
}
}
match (accessor, timeout_seconds, cases) {
(Some(accessor), Some(timeout_seconds), Some(cases)) => {
let cases: BTreeMap<String, TestCase> =
cases.into_iter().map(|c| (c.key.clone(), c)).collect();
Ok(ProgramSpec { accessor, timeout_seconds, cases })
}
(None, _, _) => Err(ComponentError::MissingRequiredKey("accessor")),
(_, None, _) => Err(ComponentError::MissingRequiredKey("timeout_seconds")),
(_, _, None) => Err(ComponentError::MissingRequiredKey("cases")),
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum Accessor {
All,
Feedback,
Legacy,
}
#[derive(Debug, PartialEq)]
pub(crate) struct TestCase {
/// The unique key for this test case inside this suite.
pub key: String,
/// The selector that will be used to read from the Archivist for this test case.
pub selector: String,
/// If set, this triage expression is applied against the value returned by the selector.
/// Otherwise, the test passes so long as any value is selected.
pub expression: Option<String>,
}
impl std::string::ToString for TestCase {
fn to_string(&self) -> String {
self.key.clone()
}
}
impl TryFrom<String> for TestCase {
type Error = ComponentError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let splits: Vec<&str> = value.split(" WHERE ").collect();
if splits.len() == 1 {
let selector = splits[0].to_string();
selectors::parse_selector::<VerboseError>(&selector).map_err(|e| {
ComponentError::InvalidTestCase { value: value.clone(), reason: e.to_string() }
})?;
Ok(TestCase { key: value, selector, expression: None })
} else if splits.len() == 2 {
let selector = splits[0].to_string();
let expression = Some(splits[1].to_string());
selectors::parse_selector::<VerboseError>(&selector).map_err(|e| {
ComponentError::InvalidTestCase { value: value.clone(), reason: e.to_string() }
})?;
Ok(TestCase { key: value, selector, expression })
} else {
Err(ComponentError::InvalidTestCase {
value,
reason: "too many 'WHERE' clauses".to_string(),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug)]
struct DictBuilder {
accessor: Option<String>,
timeout_seconds: Option<String>,
cases: Vec<String>,
}
impl DictBuilder {
fn new() -> Self {
DictBuilder { accessor: None, timeout_seconds: None, cases: vec![] }
}
fn with_accessor(mut self, value: impl Into<String>) -> Self {
self.accessor = Some(value.into());
self
}
fn with_timeout_seconds(mut self, value: impl Into<String>) -> Self {
self.timeout_seconds = Some(value.into());
self
}
fn add_case(mut self, value: impl Into<String>) -> Self {
self.cases.push(value.into());
self
}
fn build(self) -> fdata::Dictionary {
let mut ret = fdata::Dictionary { entries: Some(vec![]), ..Default::default() };
let mut add_value = |key: &str, value: fdata::DictionaryValue| {
ret.entries.as_mut().expect("entries exists").push(fdata::DictionaryEntry {
key: key.to_string(),
value: Some(Box::new(value)),
});
};
if let Some(val) = self.accessor {
add_value("accessor", fdata::DictionaryValue::Str(val));
}
if let Some(val) = self.timeout_seconds {
add_value("timeout_seconds", fdata::DictionaryValue::Str(val));
}
if self.cases.len() > 0 {
add_value("cases", fdata::DictionaryValue::StrVec(self.cases));
}
ret
}
}
#[test]
fn empty_dictionary_fails() {
assert_eq!(
ComponentError::MissingRequiredKey("program"),
ProgramSpec::try_from(fdata::Dictionary::default()).unwrap_err()
);
}
#[test]
fn fields_are_present() {
assert_eq!(
ComponentError::MissingRequiredKey("accessor"),
ProgramSpec::try_from(DictBuilder::new().build()).unwrap_err(),
);
assert_eq!(
ComponentError::MissingRequiredKey("timeout_seconds"),
ProgramSpec::try_from(DictBuilder::new().with_accessor("ALL").build()).unwrap_err(),
);
assert_eq!(
ComponentError::MissingRequiredKey("cases"),
ProgramSpec::try_from(
DictBuilder::new().with_accessor("ALL").with_timeout_seconds("10").build()
)
.unwrap_err(),
);
assert!(ProgramSpec::try_from(
DictBuilder::new()
.with_accessor("ALL")
.with_timeout_seconds("10")
.add_case("a:b:c")
.build()
)
.is_ok());
}
#[test]
fn accessor_validation() {
let base_dict = DictBuilder::new().with_timeout_seconds("10").add_case("a:b:c");
assert_eq!(
ComponentError::UnknownAccessorValue("Str(\"SOMETHING\")".to_string()),
ProgramSpec::try_from(base_dict.clone().with_accessor("SOMETHING").build())
.unwrap_err(),
);
for (name, value) in vec![
("ALL", Accessor::All),
("FEEDBACK", Accessor::Feedback),
("LEGACY", Accessor::Legacy),
] {
let spec = ProgramSpec::try_from(base_dict.clone().with_accessor(name).build())
.expect("validate accessor");
assert_eq!(spec.accessor, value);
}
}
#[test]
fn timeout_seconds_validation() {
let base_dict = DictBuilder::new().with_accessor("ALL").add_case("a:b:c");
assert_eq!(
ComponentError::WrongType("timeout_seconds", "numeric string"),
ProgramSpec::try_from(base_dict.clone().with_timeout_seconds("invalid").build())
.unwrap_err()
);
let spec = ProgramSpec::try_from(base_dict.clone().with_timeout_seconds("100").build())
.expect("validate timeout_seconds");
assert_eq!(spec.timeout_seconds, 100);
}
#[test]
fn test_case_validation() {
let base_dict = DictBuilder::new().with_accessor("ALL").with_timeout_seconds("10");
assert_eq!(
ComponentError::InvalidTestCase {
value: "a WHERE b WHERE c".to_string(),
reason: "too many 'WHERE' clauses".to_string(),
},
ProgramSpec::try_from(base_dict.clone().add_case("a WHERE b WHERE c").build())
.unwrap_err()
);
// Test that selector parsing catches invalid selectors.
assert!(ProgramSpec::try_from(base_dict.clone().add_case("a:b:c:d").build()).is_err());
assert!(ProgramSpec::try_from(base_dict.clone().add_case("a").build()).is_err());
let spec = ProgramSpec::try_from(
base_dict.clone().add_case("a:b:c").add_case("a:b:c WHERE [d] d > 0").build(),
)
.expect("validate cases");
assert_eq!(
spec.cases["a:b:c"],
TestCase { key: "a:b:c".to_string(), selector: "a:b:c".to_string(), expression: None }
);
assert_eq!(
spec.cases["a:b:c WHERE [d] d > 0"],
TestCase {
key: "a:b:c WHERE [d] d > 0".to_string(),
selector: "a:b:c".to_string(),
expression: Some("[d] d > 0".to_string())
}
);
}
}