#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use kitt_score::clock::TestClock;
use kitt_score::event::{ActionIngest, AttrSet, KindRef, StateUpdate, Trigger};
use kitt_score::location::LocationDef;
use kitt_score::schema::attr::{AttrType, Value};
use kitt_score::{ActionId, Engine, Ingested, LocId, SchemaBuilder, ScorerSpec};
use smallvec::smallvec;
const fn ingested_debug<T>(i: &Ingested<T>) -> &'static str {
match i {
Ingested::Updated => "Updated",
Ingested::Registered(_) => "Registered",
Ingested::Decided(_) => "Decided",
Ingested::NoWinner => "NoWinner",
Ingested::ReloadInProgress => "ReloadInProgress",
Ingested::Rejected(_) => "Rejected",
}
}
#[test]
fn predicate_bytecode_matches_age_gender_audience_groups() {
let mut b = SchemaBuilder::new();
let kid_audience = b.kind(
"audience",
&[
("male_20_30_frac", AttrType::F32),
("female_20_40_frac", AttrType::F32),
("other_frac", AttrType::F32),
],
);
let schema = b.build();
let aid_male_20_30 = schema.attr("male_20_30_frac").unwrap();
let aid_female_20_40 = schema.attr("female_20_40_frac").unwrap();
let aid_other = schema.attr("other_frac").unwrap();
let clock = TestClock::at(10_000);
let engine: Engine<&'static str> = Engine::builder()
.schema(schema)
.clock_arc(clock.clone())
.build()
.unwrap();
let loc = LocId(7);
engine
.upsert_location(&LocationDef {
id: loc,
kinds_allowed: vec![kid_audience],
ref_attrs: vec![],
})
.unwrap();
let empty_trigger = Trigger {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet::new(),
};
assert!(matches!(
engine.ingest_trigger(empty_trigger),
Ingested::NoWinner
));
let reg_1 = engine.ingest_action(ActionIngest {
location: loc,
action_id: ActionId::from("101"),
start: 9_000,
end: 20_000,
priority: 0,
kind: KindRef::Id(kid_audience),
scorer: ScorerSpec::Predicate("$audience.male_20_30_frac"),
payload: "creative-young-male",
post: None,
});
assert!(matches!(reg_1, Ingested::Registered(id) if id == ActionId::from("101")));
let reg_2 = engine.ingest_action(ActionIngest {
location: loc,
action_id: ActionId::from("202"),
start: 9_000,
end: 20_000,
priority: 0,
kind: KindRef::Id(kid_audience),
scorer: ScorerSpec::Predicate("$audience.female_20_40_frac"),
payload: "creative-female-adult",
post: None,
});
assert!(matches!(reg_2, Ingested::Registered(id) if id == ActionId::from("202")));
let first_update = StateUpdate {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet {
entries: smallvec![
(aid_male_20_30, Value::F32(0.62)),
(aid_female_20_40, Value::F32(0.21)),
(aid_other, Value::F32(0.17)),
],
},
};
assert!(matches!(
engine.ingest_update(first_update),
Ingested::Updated
));
let first_trigger = Trigger {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet::new(),
};
match engine.ingest_trigger(first_trigger) {
Ingested::Decided(outcome) => {
assert_eq!(outcome.action_id, ActionId::from("101"));
assert_eq!(outcome.payload, "creative-young-male");
assert!((outcome.score.score - 0.62).abs() < 1e-6);
assert_eq!(outcome.score.priority, 0);
}
other => panic!("expected first decision, got {}", ingested_debug(&other)),
}
let second_update = StateUpdate {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet {
entries: smallvec![
(aid_male_20_30, Value::F32(0.18)),
(aid_female_20_40, Value::F32(0.67)),
(aid_other, Value::F32(0.15)),
],
},
};
assert!(matches!(
engine.ingest_update(second_update),
Ingested::Updated
));
let second_trigger = Trigger {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet::new(),
};
match engine.ingest_trigger(second_trigger) {
Ingested::Decided(outcome) => {
assert_eq!(outcome.action_id, ActionId::from("202"));
assert_eq!(outcome.payload, "creative-female-adult");
assert!((outcome.score.score - 0.67).abs() < 1e-6);
assert_eq!(outcome.score.priority, 0);
}
other => panic!("expected second decision, got {}", ingested_debug(&other)),
}
clock.set(25_000);
let expired_trigger = Trigger {
location: loc,
kind: KindRef::Id(kid_audience),
attrs: AttrSet::new(),
};
assert!(matches!(
engine.ingest_trigger(expired_trigger),
Ingested::NoWinner
));
}