Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
use std::collections::HashSet;
use std::sync::Arc;
use chrono::Utc;
use serde_json::json;
use crate::error::{NimbusError, Result};
use crate::stateful::behavior::{
EventStore, Interval, IntervalConfig, IntervalData, MultiIntervalCounter, SingleIntervalCounter,
};
use crate::stateful::targeting::RecordedContext;
use crate::tests::helpers::TestRecordedContext;
use crate::{AppContext, NimbusTargetingHelper, TargetingAttributes};
#[test]
fn test_event_sum_transform() {
let counter = MultiIntervalCounter::new(vec![SingleIntervalCounter {
data: IntervalData {
bucket_count: 3,
starting_instant: Utc::now(),
buckets: vec![1, 1, 0].into(),
},
config: IntervalConfig::new(3, Interval::Days),
}]);
let event_store = EventStore::from(vec![("app.foregrounded".to_string(), counter)]);
let th = NimbusTargetingHelper::from(event_store);
assert!(
!th.eval_jexl("'app.foregrounded'|eventSum('Days', 3, 0) > 2")
.unwrap(),
);
assert!(
th.eval_jexl("'app.foregrounded'|eventSum('Days', 3, 0) > 1")
.unwrap(),
);
}
#[test]
fn test_event_count_non_zero_transform() {
let counter = MultiIntervalCounter::new(vec![SingleIntervalCounter {
data: IntervalData {
bucket_count: 3,
starting_instant: Utc::now(),
buckets: vec![1, 2, 0].into(),
},
config: IntervalConfig::new(3, Interval::Days),
}]);
let event_store = EventStore::from(vec![("app.foregrounded".to_string(), counter)]);
let th = NimbusTargetingHelper::from(event_store);
assert!(
!th.eval_jexl("'app.foregrounded'|eventCountNonZero('Days', 3, 0) > 2")
.unwrap(),
);
assert!(
th.eval_jexl("'app.foregrounded'|eventCountNonZero('Days', 3, 0) > 1")
.unwrap()
);
}
#[test]
fn test_event_average_per_interval_transform() {
let counter = MultiIntervalCounter::new(vec![SingleIntervalCounter {
data: IntervalData {
bucket_count: 3,
starting_instant: Utc::now(),
buckets: vec![1, 2, 0, 0, 0, 2, 3].into(),
},
config: IntervalConfig::new(3, Interval::Days),
}]);
let event_store = EventStore::from(vec![("app.foregrounded".to_string(), counter)]);
let th = NimbusTargetingHelper::from(event_store);
assert!(
!th.eval_jexl("'app.foregrounded'|eventAveragePerInterval('Days', 7, 0) > 2")
.unwrap(),
);
assert!(
th.eval_jexl("'app.foregrounded'|eventAveragePerInterval('Days', 7, 0) > 1.14")
.unwrap()
);
}
#[test]
fn test_event_average_per_non_zero_interval_transform() {
let counter = MultiIntervalCounter::new(vec![SingleIntervalCounter {
data: IntervalData {
bucket_count: 3,
starting_instant: Utc::now(),
buckets: vec![1, 2, 0, 0, 0, 2, 4].into(),
},
config: IntervalConfig::new(3, Interval::Days),
}]);
let event_store = EventStore::from(vec![("app.foregrounded".to_string(), counter)]);
let th = NimbusTargetingHelper::from(event_store);
assert!(
!th.eval_jexl("'app.foregrounded'|eventAveragePerNonZeroInterval('Days', 7, 0) == 1")
.unwrap(),
);
assert!(
th.eval_jexl("'app.foregrounded'|eventAveragePerNonZeroInterval('Days', 7, 0) == 2.25")
.unwrap()
);
}
#[test]
fn test_event_transform_sum_cnz_avg_avgnz_parameters() {
let th = NimbusTargetingHelper::default();
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventSum('Days') > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform Sum requires 2-3 parameters"
));
assert!(matches!(
th.eval_jexl("1|eventSum('Days', 3, 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: JSON Error: event = nimbus::stateful::behavior::EventQueryType::validate_counting_arguments::serde_json::from_value — invalid type: floating point `1.0`, expected a string"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventSum(1, 3, 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: JSON Error: interval = nimbus::stateful::behavior::EventQueryType::validate_counting_arguments::serde_json::from_value — invalid type: floating point `1.0`, expected a string"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventSum('Day', 3, 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Behavior error: IntervalParseError: Day is not a valid Interval"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventSum('Days', 'test', 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform Sum requires a positive number as the second parameter"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventSum('Days', 3, 'test') > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform Sum requires a positive number as the third parameter"
));
}
#[test]
fn test_event_transform_last_seen_parameters() {
let th = NimbusTargetingHelper::default();
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventLastSeen() > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform LastSeen requires 1-2 parameters"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventLastSeen('Days', 0, 10) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform LastSeen requires 1-2 parameters"
));
assert!(matches!(
th.eval_jexl("1|eventLastSeen('Days', 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: JSON Error: event = nimbus::stateful::behavior::EventQueryType::validate_last_seen_arguments::serde_json::from_value — invalid type: floating point `1.0`, expected a string"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventLastSeen(1, 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: JSON Error: interval = nimbus::stateful::behavior::EventQueryType::validate_last_seen_arguments::serde_json::from_value — invalid type: floating point `1.0`, expected a string"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventLastSeen('Day', 0) > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Behavior error: IntervalParseError: Day is not a valid Interval"
));
assert!(matches!(
th.eval_jexl("'app.foregrounded'|eventLastSeen('Days', 'test') > 1").unwrap_err(),
NimbusError::EvaluationError(e)
if e == "Custom error: Transform parameter error: event transform LastSeen requires a positive number as the second parameter"
));
assert!(
th.eval_jexl("'app_cycle.foreground1'|eventLastSeen('Days', 2) > 1")
.unwrap()
);
}
#[test]
fn test_targeting_active_experiments_equivalency() {
// Here's our valid jexl statement
let expression_statement = "'test' in active_experiments";
// A matching context that includes the appropriate specific context
let mut targeting_attributes = TargetingAttributes::from(AppContext {
app_name: "nimbus_test".to_string(),
app_id: "1010".to_string(),
channel: "test".to_string(),
app_version: Some("4.4".to_string()),
app_build: Some("1234".to_string()),
architecture: Some("x86_64".to_string()),
device_manufacturer: Some("Samsung".to_string()),
device_model: Some("Galaxy S10".to_string()),
locale: Some("en-US".to_string()),
os: Some("Android".to_string()),
os_version: Some("10".to_string()),
android_sdk_version: Some("29".to_string()),
debug_tag: None,
custom_targeting_attributes: None,
..Default::default()
});
let mut set = HashSet::<String>::new();
set.insert("test".into());
targeting_attributes.active_experiments = set;
// The targeting should pass!
assert!(
NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap()
);
// We set active_experiment treatment to something not expected and try again
let mut set = HashSet::<String>::new();
set.insert("test1".into());
targeting_attributes.active_experiments = set;
assert!(
!NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap(),
);
// We set active_experiments to None and try again
let set = HashSet::<String>::new();
targeting_attributes.active_experiments = set;
assert!(
!NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap(),
);
}
#[test]
fn test_targeting_active_experiments_exists() {
// Here's our valid jexl statement
let expression_statement = "'test' in active_experiments";
// A matching context that includes the appropriate specific context
let mut targeting_attributes = TargetingAttributes::from(AppContext {
app_name: "nimbus_test".to_string(),
app_id: "1010".to_string(),
channel: "test".to_string(),
app_version: Some("4.4".to_string()),
app_build: Some("1234".to_string()),
architecture: Some("x86_64".to_string()),
device_manufacturer: Some("Samsung".to_string()),
device_model: Some("Galaxy S10".to_string()),
locale: Some("en-US".to_string()),
os: Some("Android".to_string()),
os_version: Some("10".to_string()),
android_sdk_version: Some("29".to_string()),
debug_tag: None,
custom_targeting_attributes: None,
..Default::default()
});
let mut set = HashSet::<String>::new();
set.insert("test".into());
targeting_attributes.active_experiments = set;
// The targeting should pass!
assert!(
NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap()
);
// We set active_experiment treatment to something not expected and try again
let set = HashSet::<String>::new();
targeting_attributes.active_experiments = set;
assert!(
!NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap(),
);
}
#[test]
fn test_targeting_is_already_enrolled() {
// Here's our valid jexl statement
let expression_statement = "is_already_enrolled";
// A matching context that includes the appropriate specific context
let mut targeting_attributes = TargetingAttributes::from(AppContext {
app_name: "nimbus_test".to_string(),
app_id: "1010".to_string(),
channel: "test".to_string(),
app_version: Some("4.4".to_string()),
app_build: Some("1234".to_string()),
custom_targeting_attributes: None,
..Default::default()
});
targeting_attributes.is_already_enrolled = true;
// The targeting should pass!
assert!(
NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap(),
);
// We make the is_already_enrolled false and try again
targeting_attributes.is_already_enrolled = false;
assert!(
!NimbusTargetingHelper::from(targeting_attributes.clone())
.eval_jexl(expression_statement)
.unwrap(),
);
}
#[test]
fn test_bucket_sample() {
fn eval_bucketsample_expr(start: &str, count: &str, total: &str) -> Result<bool> {
let expr = format!("0|bucketSample({start}, {count}, {total})");
NimbusTargetingHelper::from(AppContext {
app_name: "nimbus_test".into(),
app_id: "nimbus-test".into(),
channel: "test".into(),
..Default::default()
})
.eval_jexl(&expr)
}
let ok_cases = [
("1.1", "1000", "1000", true),
("0", "1.1", "1000", false),
("0", "0", "1000.1", false),
];
for (start, count, total, expected) in ok_cases {
assert_eq!(
eval_bucketsample_expr(start, count, total).unwrap(),
expected
);
}
let err_cases = [
(
"4294967296",
"1",
"4294967297",
"Custom error: start is out of range",
),
(
"0",
"4294967296",
"4294967296",
"Custom error: count is out of range",
),
(
"0",
"0",
"4294967296",
"Custom error: total is out of range",
),
(
r#""hello""#,
"0",
"1000",
"Custom error: start is not a number",
),
(
"0",
r#""hello""#,
"1000",
"Custom error: count is not a number",
),
(
"0",
"1000",
r#""hello""#,
"Custom error: total is not a number",
),
];
for (start, count, total, expected) in err_cases {
assert!(matches!(
eval_bucketsample_expr(start, count, total).unwrap_err(),
NimbusError::EvaluationError(e) if e == expected));
}
}
#[test]
fn test_multiple_contexts_flatten() -> crate::Result<()> {
let recorded_context = Arc::new(TestRecordedContext::new());
recorded_context.set_context(json!({
"locale": "de-CA",
"language": "de",
}));
let mut targeting_attributes =
crate::tests::test_evaluator::ta_with_locale("en-US".to_string());
targeting_attributes.set_recorded_context(recorded_context.to_json());
let value = serde_json::to_value(targeting_attributes).unwrap();
assert_eq!(value.get("locale").unwrap(), "de-CA");
assert_eq!(value.get("language").unwrap(), "de");
Ok(())
}