use proptest::prelude::*;
use prosaic_core::{Context, Engine, Session, Strictness, Value, Variation};
use prosaic_grammar_en::English;
fn base_engine() -> Engine {
Engine::new(English::new())
.strictness(Strictness::Strict)
.variation(Variation::Fixed)
}
fn safe_ident() -> impl Strategy<Value = String> {
"[A-Za-z]{1,16}".prop_map(|s| s)
}
fn leaks_open_brace(s: &str) -> bool {
s.contains('{')
}
fn leaks_close_brace(s: &str) -> bool {
s.contains('}')
}
proptest! {
#[test]
fn render_never_leaks_slot_markers(name in safe_ident()) {
let mut engine = base_engine();
engine.register_template("t", "Hello {name}").unwrap();
let mut ctx = Context::new();
ctx.insert("name", Value::String(name.clone()));
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
prop_assert!(!leaks_open_brace(&output));
prop_assert!(!leaks_close_brace(&output));
prop_assert!(output.contains(&name));
}
#[test]
fn strict_mode_errors_on_missing_slot(_seed in any::<u32>()) {
let mut engine = base_engine();
engine.register_template("t", "needs {absent}").unwrap();
let ctx = Context::new();
let mut session = Session::new();
prop_assert!(engine.render(&mut session, "t", &ctx).is_err());
}
#[test]
fn silent_mode_never_panics_on_missing_slot(_seed in "[A-Za-z]{1,8}") {
let mut engine = base_engine().strictness(Strictness::Silent);
engine.register_template("t", "hello {missing} world").unwrap();
let ctx = Context::new();
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
prop_assert!(!leaks_open_brace(&output));
prop_assert!(!leaks_close_brace(&output));
prop_assert!(!output.is_empty());
}
#[test]
fn clause_reduction_preserves_entity(name in safe_ident()) {
let mut engine = base_engine();
engine
.register_template("renamed", "{name|refer} was renamed")
.unwrap();
engine
.register_template("modified", "{name|refer} was modified")
.unwrap();
engine
.register_template("moved", "{name|refer} was moved")
.unwrap();
let mut ctx = Context::new();
ctx.insert("entity_type", Value::String("class".into()));
ctx.insert("name", Value::String(name.clone()));
let events: Vec<(&str, Context)> = vec![
("renamed", ctx.clone()),
("modified", ctx.clone()),
("moved", ctx.clone()),
];
let mut session = Session::new();
let output = engine.render_batch(&mut session, &events).unwrap();
prop_assert!(output.contains(&name));
}
#[test]
fn length_budget_keeps_pieces_within_margin(
budget in 40usize..120,
count in 1i64..12
) {
let mut engine = base_engine().max_sentence_length(budget);
engine
.register_template(
"t",
"The class UserService was renamed to AccountService, \
which impacts {count} direct {count|pluralize:consumer} including \
ProfileComponent, SettingsComponent, AdminModule, DashboardModule",
)
.unwrap();
let mut ctx = Context::new();
ctx.insert("count", Value::Number(count));
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
for piece in output.split(". ") {
prop_assert!(piece.chars().count() <= budget + 60);
}
}
#[test]
fn quantify_pipe_is_total(count in -1000i64..1_000_000) {
let mut engine = base_engine();
engine.register_template("t", "{n|quantify} caller").unwrap();
let mut ctx = Context::new();
ctx.insert("n", Value::Number(count));
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
prop_assert!(!output.is_empty());
prop_assert!(!leaks_open_brace(&output));
prop_assert!(!leaks_close_brace(&output));
}
#[test]
fn hedge_pipe_handles_any_score(score in -10_000i64..10_000) {
let mut engine = base_engine();
engine.register_template("t", "It {c|hedge} works").unwrap();
let mut ctx = Context::new();
ctx.insert("c", Value::Number(score));
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
prop_assert!(!leaks_open_brace(&output));
prop_assert!(!leaks_close_brace(&output));
prop_assert!(!output.is_empty());
}
#[test]
fn relative_time_pipe_is_total(
reference in 0i64..2_000_000_000,
ts in 0i64..2_000_000_000
) {
let mut engine = base_engine().reference_time(reference);
engine.register_template("t", "event was {ts|relative}").unwrap();
let mut ctx = Context::new();
ctx.insert("ts", Value::Number(ts));
let mut session = Session::new();
let output = engine.render(&mut session, "t", &ctx).unwrap();
prop_assert!(!output.is_empty());
prop_assert!(!leaks_open_brace(&output));
prop_assert!(!leaks_close_brace(&output));
}
}