use miette::SourceSpan;
use crate::{
enc_regex::EncodableRegex,
query::{
Cmp, Expr, Filter, FilterOrIfDef, ParamDeclaration, ParamType, ParamValue,
ParseProvidedParamsError, ProvidedParam, ProvidedParams, Query, RelativeTime, ResolveError,
StringFragment, TagType, TerminalParamType, TimeUnit,
},
tags::TagValue,
types::Dataset,
};
#[test]
fn provided_params_parse() {
let mpl_params = vec![
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "dataset".to_string(),
typ: ParamType::Terminal(TerminalParamType::Dataset),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "duration".to_string(),
typ: ParamType::Terminal(TerminalParamType::Duration),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "string".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::String)),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "int".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Int)),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "float".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Float)),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "bool".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Bool)),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "regex".to_string(),
typ: ParamType::Terminal(TerminalParamType::Regex),
},
];
let query_params = [
("lol", "whatever, this should be ignored"),
("param__dataset", "`my-metrics`"),
("param__duration", "42s"),
("param__string", "\"bar\""),
("param__int", "42"),
("param__float", "42.0"),
("param__bool", "true"),
("param__regex", "#/[a-z]+/"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<(String, String)>>();
let (provided_params, _) = ProvidedParams::parse_and_validate(&mpl_params, &query_params)
.expect("failed to parse provided params");
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "dataset".to_string(),
value: ParamValue::Dataset(Dataset::new("my-metrics".to_string()))
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "duration".to_string(),
value: ParamValue::Duration(RelativeTime {
value: 42,
unit: TimeUnit::Second
})
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "string".to_string(),
value: ParamValue::String("bar".to_string())
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "int".to_string(),
value: ParamValue::Int(42)
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "float".to_string(),
value: ParamValue::Float(42.0)
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "bool".to_string(),
value: ParamValue::Bool(true)
}));
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "regex".to_string(),
value: ParamValue::Regex(EncodableRegex::new("[a-z]+").expect("invalid regex"))
}));
assert_eq!(7, provided_params.as_slice().len());
}
#[test]
fn provided_params_parse_array() {
let mpl_params = vec![ParamDeclaration {
span: SourceSpan::from(0..0),
name: "hosts".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Array)),
}];
let query_params = vec![(
"param__hosts".to_string(),
"[\"a\", 1, 2.5, true, [2]]".to_string(),
)];
let (provided_params, _) = ProvidedParams::parse_and_validate(&mpl_params, &query_params)
.expect("failed to parse provided array param");
assert!(provided_params.as_slice().contains(&ProvidedParam {
name: "hosts".to_string(),
value: ParamValue::Array(vec![
TagValue::String("a".try_into().expect("valid shared string")),
TagValue::Int(1),
TagValue::Float(2.5),
TagValue::Bool(true),
TagValue::Array(vec![TagValue::Int(2)]),
])
}));
}
#[test]
fn provided_params_array_rejects_scalar_value() {
let mpl_params = vec![ParamDeclaration {
span: SourceSpan::from(0..0),
name: "hosts".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Array)),
}];
let query_params = vec![("param__hosts".to_string(), "42".to_string())];
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::ParseParam {
param_name,
expected_type,
err,
}) => {
assert_eq!("hosts", param_name);
assert_eq!(
ParamType::Terminal(TerminalParamType::Tag(TagType::Array)),
expected_type
);
assert!(
err.to_string().contains("declared as type array"),
"expected an array type mismatch, got: {err}"
);
}
res => panic!("expected parse param error, got {res:?}"),
}
}
#[test]
fn provided_params_array_rejects_interpolated_element() {
let mpl_params = vec![ParamDeclaration {
span: SourceSpan::from(0..0),
name: "hosts".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Array)),
}];
let query_params = vec![("param__hosts".to_string(), "[\"${ x }\"]".to_string())];
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::ParseParam { err, .. }) => {
assert!(
err.to_string().contains("Unsupported Array element"),
"expected an unsupported-element error, got: {err}"
);
}
res => panic!("expected parse param error, got {res:?}"),
}
}
#[test]
fn resolve_array_param_to_const_array() {
let provided_params = ProvidedParams::new(vec![ProvidedParam::new(
"hosts",
ParamValue::Array(vec![TagValue::Int(1), TagValue::Int(2)]),
)]);
let value = provided_params
.inline_params(Expr::Param {
span: SourceSpan::from(0..0),
param: ParamDeclaration {
span: SourceSpan::from(0..0),
name: "hosts".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::Array)),
},
})
.expect("expected array param to resolve");
assert_eq!(
value,
Expr::Const(TagValue::Array(vec![TagValue::Int(1), TagValue::Int(2)]))
);
}
#[test]
fn provided_array_param_end_to_end_in_filter() {
let (query, _warnings) = crate::compile(
"param $hosts: array;\nds:m | where t in $hosts",
std::collections::HashMap::new(),
)
.expect("`in $hosts` should compile");
let Query::Simple {
params, filters, ..
} = query
else {
panic!("expected a simple query");
};
let (provided, _) = ProvidedParams::parse_and_validate(
¶ms,
&[("param__hosts".to_string(), "[1, 2]".to_string())],
)
.expect("provided array value should validate");
let FilterOrIfDef::Filter(Filter::Cmp {
rhs: Cmp::In(expr), ..
}) = &filters[0]
else {
panic!("expected an in-filter");
};
let inlined = provided
.inline_params(expr.clone())
.expect("array param should inline");
assert_eq!(
inlined,
Expr::Const(TagValue::Array(vec![TagValue::Int(1), TagValue::Int(2)]))
);
}
#[test]
fn provided_params_duplicates() {
let mpl_params = vec![
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "dataset".to_string(),
typ: ParamType::Terminal(TerminalParamType::Dataset),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "__interval".to_string(),
typ: ParamType::Terminal(TerminalParamType::Duration),
},
];
let query_params = [
("lol", "whatever, this should be ignored"),
("param__dataset", "my-metrics"),
("param__dataset", "my-metrics-2"),
("param____interval", "1m"),
("param____interval", "5m"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<(String, String)>>();
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::ParamsProvidedMoreThanOnce(list)) => {
assert_eq!(2, list.as_slice().len());
assert!(list.as_slice().contains(&"dataset".to_string()));
assert!(list.as_slice().contains(&"__interval".to_string()));
}
res => panic!("expected duplicate params error, got {res:?}"),
}
}
#[test]
fn provided_params_declared_but_not_provided() {
let mpl_params = vec![ParamDeclaration {
span: SourceSpan::from(0..0),
name: "dataset".to_string(),
typ: ParamType::Terminal(TerminalParamType::Dataset),
}];
let query_params = [("lol", "whatever, this should be ignored")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<(String, String)>>();
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::ParamsDeclaredButNotProvided(list)) => {
assert_eq!(1, list.as_slice().len());
assert!(list.as_slice().contains(&"dataset".to_string()));
}
res => panic!("expected params declared but not provided error, got {res:?}"),
}
}
#[test]
fn provided_params_not_declared() {
let mpl_params = vec![];
let query_params = [("param__dataset", "yo")]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<(String, String)>>();
let (_, warnings) =
ProvidedParams::parse_and_validate(&mpl_params, &query_params).expect("expected ok");
assert_eq!(1, warnings.as_slice().len());
let warning = warnings
.as_slice()
.first()
.expect("expected a warning, i just checked?");
assert!(
warning
.warning()
.to_string()
.contains("provided but not declared: $dataset")
);
}
#[test]
fn provided_params_parse_failure() {
let mpl_params = vec![
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "dataset".to_string(),
typ: ParamType::Terminal(TerminalParamType::Dataset),
},
ParamDeclaration {
span: SourceSpan::from(0..0),
name: "duration".to_string(),
typ: ParamType::Terminal(TerminalParamType::Duration),
},
];
let query_params = [
("param__dataset", "`my-dataset`"),
("param__duration", "invalid-duration"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<Vec<(String, String)>>();
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::ParseParam {
param_name,
expected_type,
err: _,
}) => {
assert_eq!("duration", param_name);
assert_eq!(
ParamType::Terminal(TerminalParamType::Duration),
expected_type
);
}
res => panic!("expected parse param error, got {res:?}"),
}
}
#[test]
fn too_many_provided_params() {
let mpl_params = vec![];
let query_params = (0..129)
.map(|i| (format!("param__dataset_{i}"), format!("value_{i}")))
.collect::<Vec<(String, String)>>();
match ProvidedParams::parse_and_validate(&mpl_params, &query_params) {
Err(ParseProvidedParamsError::TooManyParamsProvided(_)) => {
}
res => panic!("expected too many params error, got {res:?}"),
}
}
#[test]
fn resolve_tag_value_from_provided_param() {
let provided_params = ProvidedParams::new(vec![ProvidedParam::new(
"env",
ParamValue::String("prod".to_string()),
)]);
let value = provided_params
.inline_params(Expr::Param {
span: SourceSpan::from(0..0),
param: ParamDeclaration {
span: SourceSpan::from(0..0),
name: "env".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::String)),
},
})
.expect("expected string param to resolve");
assert_eq!(
value,
Expr::Const(TagValue::String(
"prod".try_into().expect("valid shared string")
))
);
}
#[test]
fn resolve_tag_value_from_provided_param_interpolated() {
let provided_params = ProvidedParams::new(vec![ProvidedParam::new(
"env",
ParamValue::String("prod".to_string()),
)]);
let value = provided_params
.inline_params(Expr::String(vec![
StringFragment::Text("http://".to_string()),
StringFragment::Expr(Expr::Param {
span: SourceSpan::from(0..0),
param: ParamDeclaration {
span: SourceSpan::from(0..0),
name: "env".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::String)),
},
}),
StringFragment::Text(":".to_string()),
StringFragment::Expr(Expr::Const(TagValue::Int(8080))),
]))
.expect("expected string param to resolve");
assert_eq!(
value,
Expr::Const(TagValue::String(
"http://prod:8080".try_into().expect("valid shared string")
))
);
}
#[test]
fn resolve_tag_value_errors_for_missing_param() {
let provided_params = ProvidedParams::new(vec![]);
match provided_params.inline_params(Expr::Param {
span: SourceSpan::from(0..0),
param: ParamDeclaration {
span: SourceSpan::from(0..0),
name: "env".to_string(),
typ: ParamType::Terminal(TerminalParamType::Tag(TagType::String)),
},
}) {
Err(ResolveError::ParamNotProvided(name)) => assert_eq!(name, "env"),
res => panic!("expected missing param error, got {res:?}"),
}
}
fn optional_string_param(name: &str) -> ParamDeclaration {
ParamDeclaration {
span: SourceSpan::from(0..0),
name: name.to_string(),
typ: ParamType::Optional(TerminalParamType::Tag(TagType::String)),
}
}
fn tag_filter(field: &str, value: &str) -> Filter {
Filter::Cmp {
field: field.to_string(),
rhs: Cmp::Eq(Expr::Const(TagValue::String(
value.try_into().expect("valid shared string"),
))),
}
}
#[test]
fn active_filter_keeps_plain_filters() {
let provided = ProvidedParams::new(vec![]);
let filter = tag_filter("env", "prod");
let filter_or_ifdef = FilterOrIfDef::Filter(filter.clone());
assert_eq!(Some(&filter), provided.active_filter(&filter_or_ifdef));
}
#[test]
fn active_filter_applies_ifdef_when_param_is_provided() {
let provided = ProvidedParams::new(vec![ProvidedParam {
name: "env".to_string(),
value: ParamValue::String("prod".to_string()),
}]);
let filter = tag_filter("env", "prod");
let filter_or_ifdef = FilterOrIfDef::Ifdef {
param: optional_string_param("env"),
filter: filter.clone(),
else_filter: None,
};
assert_eq!(Some(&filter), provided.active_filter(&filter_or_ifdef));
}
#[test]
fn active_filter_drops_ifdef_when_param_is_omitted() {
let provided = ProvidedParams::new(vec![]);
let filter_or_ifdef = FilterOrIfDef::Ifdef {
param: optional_string_param("env"),
filter: tag_filter("env", "prod"),
else_filter: None,
};
assert_eq!(None, provided.active_filter(&filter_or_ifdef));
}
#[test]
fn becomes_else_ifdef_when_param_is_omitted() {
let provided = ProvidedParams::new(vec![]);
let filter = tag_filter("env", "prod");
let filter_or_ifdef = FilterOrIfDef::Ifdef {
param: optional_string_param("env"),
filter: tag_filter("env", "prod"),
else_filter: Some(filter.clone()),
};
assert_eq!(Some(&filter), provided.active_filter(&filter_or_ifdef));
}
#[test]
fn active_filters_preserves_order_and_drops_inactive_ifdefs() {
let provided = ProvidedParams::new(vec![ProvidedParam {
name: "env".to_string(),
value: ParamValue::String("prod".to_string()),
}]);
let region = tag_filter("region", "us-east-1");
let env = tag_filter("env", "prod");
let cluster = tag_filter("cluster", "blue");
let filters = vec![
FilterOrIfDef::Filter(region.clone()),
FilterOrIfDef::Ifdef {
param: optional_string_param("env"),
filter: env.clone(),
else_filter: None,
},
FilterOrIfDef::Ifdef {
param: optional_string_param("cluster"),
filter: cluster,
else_filter: None,
},
];
assert_eq!(vec![®ion, &env], provided.active_filters(&filters));
}