mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 08:02:28 +00:00
593 lines
20 KiB
Rust
593 lines
20 KiB
Rust
use serde_json::json;
|
|
use std::sync::Arc;
|
|
use zen_engine::policy::{
|
|
EngineEdit, EvaluateRequest, PolicyWorkspace, RenameTarget, ScopeRequest, Severity,
|
|
};
|
|
use zen_expression::variable::{Variable, VariableType};
|
|
|
|
fn workspace_with(doc: serde_json::Value) -> PolicyWorkspace {
|
|
let mut ws = PolicyWorkspace::new();
|
|
ws.set_policy("p", serde_json::from_value(doc).unwrap());
|
|
ws
|
|
}
|
|
|
|
fn request(input: serde_json::Value, trace: bool) -> EvaluateRequest {
|
|
EvaluateRequest {
|
|
policy_path: Arc::from("p"),
|
|
input: Variable::from(input),
|
|
goals: Vec::new(),
|
|
trace,
|
|
}
|
|
}
|
|
|
|
fn evaluate_output(ws: &PolicyWorkspace, input: serde_json::Value) -> serde_json::Value {
|
|
let result = ws.evaluate(&request(input, false)).expect("evaluate");
|
|
result.output.into()
|
|
}
|
|
|
|
fn output_type(ws: &PolicyWorkspace, path: &str) -> VariableType {
|
|
ws.outputs(&ScopeRequest::for_policy("p"))
|
|
.into_iter()
|
|
.find(|o| o.path.as_ref() == path)
|
|
.unwrap_or_else(|| panic!("output {path} not registered"))
|
|
.resolved_type
|
|
}
|
|
|
|
fn error_messages(ws: &PolicyWorkspace) -> Vec<String> {
|
|
ws.diagnostics("p")
|
|
.into_iter()
|
|
.filter(|d| d.severity == Severity::Error)
|
|
.map(|d| d.message.to_string())
|
|
.collect()
|
|
}
|
|
|
|
fn order_dm() -> serde_json::Value {
|
|
json!({ "id": "dm", "type": "dataModel", "props": { "data": {
|
|
"name": "order",
|
|
"properties": [
|
|
{ "id": "p1", "name": "amount", "type": "number", "array": false, "optional": false },
|
|
{ "id": "p2", "name": "region", "type": "string", "enum": ["US", "EU"], "array": false, "optional": false },
|
|
{ "id": "p3", "name": "express", "type": "boolean", "array": false, "optional": false }
|
|
]
|
|
} } })
|
|
}
|
|
|
|
fn strict_table_doc() -> serde_json::Value {
|
|
json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [
|
|
{ "id": "i1", "name": "", "field": "order.amount" },
|
|
{ "id": "i2", "name": "", "field": "order.region" }
|
|
],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.shippingCost" } ],
|
|
"rules": [
|
|
{ "i1": ">= 100", "i2": "\"US\"", "o1": "0" },
|
|
{ "i1": ">= 100", "i2": "\"EU\"", "o1": "5" }
|
|
]
|
|
} } }
|
|
]
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn no_match_emits_null_for_scalar_output() {
|
|
let ws = workspace_with(strict_table_doc());
|
|
let output = evaluate_output(&ws, json!({ "order": { "amount": 50, "region": "US" } }));
|
|
assert_eq!(
|
|
output.pointer("/order/shippingCost"),
|
|
Some(&serde_json::Value::Null),
|
|
"no-match scalar column must write an explicit null; got {output:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_match_trace_has_no_matched_rows() {
|
|
let ws = workspace_with(strict_table_doc());
|
|
let result = ws
|
|
.evaluate(&request(
|
|
json!({ "order": { "amount": 50, "region": "US" } }),
|
|
true,
|
|
))
|
|
.expect("evaluate");
|
|
let trace = result.trace.expect("trace");
|
|
let dt = trace
|
|
.executions
|
|
.iter()
|
|
.find(|e| e.block_id.as_ref() == "dt")
|
|
.expect("dt execution");
|
|
let trace_json = serde_json::to_value(&dt.trace).unwrap();
|
|
assert_eq!(trace_json["matchedRows"], json!([]));
|
|
}
|
|
|
|
#[test]
|
|
fn uncovered_scalar_output_is_nullable() {
|
|
let ws = workspace_with(strict_table_doc());
|
|
assert!(error_messages(&ws).is_empty());
|
|
assert!(
|
|
matches!(
|
|
output_type(&ws, "order.shippingCost"),
|
|
VariableType::Nullable(_)
|
|
),
|
|
"no catch-all and partial coverage must produce a nullable output"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn catch_all_row_makes_scalar_output_non_nullable() {
|
|
let mut doc = strict_table_doc();
|
|
doc["blocks"][1]["props"]["data"]["rules"]
|
|
.as_array_mut()
|
|
.unwrap()
|
|
.push(json!({ "i1": "", "i2": "", "o1": "20" }));
|
|
let ws = workspace_with(doc);
|
|
assert!(error_messages(&ws).is_empty());
|
|
assert!(
|
|
matches!(output_type(&ws, "order.shippingCost"), VariableType::Number),
|
|
"catch-all row must prove coverage"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enum_union_across_rows_covers() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.region" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.zone" } ],
|
|
"rules": [
|
|
{ "i1": "\"US\"", "o1": "1" },
|
|
{ "i1": "\"EU\"", "o1": "2" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(error_messages(&ws).is_empty());
|
|
assert!(matches!(
|
|
output_type(&ws, "order.zone"),
|
|
VariableType::Number
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn partial_enum_union_stays_nullable() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.region" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.zone" } ],
|
|
"rules": [ { "i1": "\"US\"", "o1": "1" } ]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.zone"),
|
|
VariableType::Nullable(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn number_tiling_covers_and_gap_does_not() {
|
|
let tiled = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.band" } ],
|
|
"rules": [
|
|
{ "i1": "< 100", "o1": "\"low\"" },
|
|
{ "i1": "[100..500]", "o1": "\"mid\"" },
|
|
{ "i1": "> 500", "o1": "\"high\"" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(tiled);
|
|
assert!(
|
|
!matches!(output_type(&ws, "order.band"), VariableType::Nullable(_)),
|
|
"tiled number coverage must be non-nullable"
|
|
);
|
|
|
|
let gapped = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.band" } ],
|
|
"rules": [
|
|
{ "i1": "< 100", "o1": "\"low\"" },
|
|
{ "i1": "> 500", "o1": "\"high\"" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(gapped);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.band"),
|
|
VariableType::Nullable(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn bool_both_values_cover() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.express" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.fee" } ],
|
|
"rules": [
|
|
{ "i1": "true", "o1": "10" },
|
|
{ "i1": "false", "o1": "0" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.fee"),
|
|
VariableType::Number
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn multi_column_rows_do_not_prove_coverage() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [
|
|
{ "id": "i1", "name": "", "field": "order.region" },
|
|
{ "id": "i2", "name": "", "field": "order.amount" }
|
|
],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.zone" } ],
|
|
"rules": [
|
|
{ "i1": "\"US\"", "i2": "> 0", "o1": "1" },
|
|
{ "i1": "\"EU\"", "i2": "> 0", "o1": "2" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.zone"),
|
|
VariableType::Nullable(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn optional_input_field_is_not_covered_without_catch_all() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
{ "id": "dm", "type": "dataModel", "props": { "data": {
|
|
"name": "order",
|
|
"properties": [
|
|
{ "id": "p2", "name": "region", "type": "string", "enum": ["US", "EU"], "array": false, "optional": true }
|
|
]
|
|
} } },
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.region" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.zone" } ],
|
|
"rules": [
|
|
{ "i1": "\"US\"", "o1": "1" },
|
|
{ "i1": "\"EU\"", "o1": "2" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.zone"),
|
|
VariableType::Nullable(_)
|
|
));
|
|
}
|
|
|
|
fn mixed_table_doc() -> serde_json::Value {
|
|
json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [
|
|
{ "id": "o1", "name": "", "field": "order.tags[]" },
|
|
{ "id": "o2", "name": "", "field": "order.tier" }
|
|
],
|
|
"rules": [
|
|
{ "i1": "> 1000", "o1": "\"vip\"", "o2": "" },
|
|
{ "i1": "> 100", "o1": "\"bulk\"", "o2": "\"gold\"" },
|
|
{ "i1": "> 10", "o1": "", "o2": "\"silver\"" },
|
|
{ "i1": "", "o1": "\"standard\"", "o2": "\"bronze\"" }
|
|
]
|
|
} } }
|
|
]
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_table_collects_tags_and_first_matches_tier() {
|
|
let ws = workspace_with(mixed_table_doc());
|
|
assert!(error_messages(&ws).is_empty(), "{:?}", error_messages(&ws));
|
|
|
|
let output = evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "amount": 5000, "region": "US", "express": false } }),
|
|
);
|
|
assert_eq!(
|
|
output.pointer("/order/tags"),
|
|
Some(&json!(["vip", "bulk", "standard"]))
|
|
);
|
|
assert_eq!(output.pointer("/order/tier"), Some(&json!("gold")));
|
|
|
|
let output = evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "amount": 50, "region": "US", "express": false } }),
|
|
);
|
|
assert_eq!(output.pointer("/order/tags"), Some(&json!(["standard"])));
|
|
assert_eq!(output.pointer("/order/tier"), Some(&json!("silver")));
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_falls_through_rows_with_empty_cell() {
|
|
let ws = workspace_with(mixed_table_doc());
|
|
let output = evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "amount": 5000, "region": "US", "express": false } }),
|
|
);
|
|
assert_eq!(
|
|
output.pointer("/order/tier"),
|
|
Some(&json!("gold")),
|
|
"row 0 matches with an empty tier cell; tier must fall through to row 1"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_table_types_are_array_and_covered_scalar() {
|
|
let ws = workspace_with(mixed_table_doc());
|
|
let tags = output_type(&ws, "order.tags");
|
|
assert!(
|
|
matches!(&tags, VariableType::Array(inner) if !matches!(inner.as_ref(), VariableType::Nullable(_))),
|
|
"collect column must type as a clean array, got {tags}"
|
|
);
|
|
let tier = output_type(&ws, "order.tier");
|
|
assert!(
|
|
!matches!(tier, VariableType::Nullable(_)),
|
|
"catch-all row provides tier coverage, got {tier}"
|
|
);
|
|
assert_eq!(format!("{tier}"), "\"gold\" | \"silver\" | \"bronze\"");
|
|
}
|
|
|
|
#[test]
|
|
fn no_match_collect_emits_empty_array() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.tags[]" } ],
|
|
"rules": [ { "i1": "> 1000", "o1": "\"vip\"" } ]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
let output = evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "amount": 5, "region": "US", "express": false } }),
|
|
);
|
|
assert_eq!(output.pointer("/order/tags"), Some(&json!([])));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_collect_hit_policy_marks_all_columns() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"hitPolicy": "collect",
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.tags" } ],
|
|
"rules": [
|
|
{ "i1": "> 10", "o1": "\"a\"" },
|
|
{ "i1": "> 100", "o1": "\"b\"" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
assert!(matches!(
|
|
output_type(&ws, "order.tags"),
|
|
VariableType::Array(_)
|
|
));
|
|
let output = evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "amount": 500, "region": "US", "express": false } }),
|
|
);
|
|
assert_eq!(output.pointer("/order/tags"), Some(&json!(["a", "b"])));
|
|
}
|
|
|
|
#[test]
|
|
fn collect_marker_mid_path_errors() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.tags[].x" } ],
|
|
"rules": [ { "i1": "", "o1": "\"a\"" } ]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
let errors = error_messages(&ws);
|
|
assert!(
|
|
errors.iter().any(|m| m.contains("[]")),
|
|
"mid-path [] must raise InvalidWritePath, got {errors:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bare_collect_marker_errors() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "[]" } ],
|
|
"rules": [ { "i1": "", "o1": "\"a\"" } ]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
let errors = error_messages(&ws);
|
|
assert!(
|
|
!errors.is_empty(),
|
|
"bare [] field must raise InvalidWritePath"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rename_rewrites_collect_field_keeping_marker() {
|
|
let ws = workspace_with(mixed_table_doc());
|
|
let edits = ws.rename(
|
|
&RenameTarget::Field {
|
|
entity: Arc::from("order"),
|
|
field: Arc::from("tags"),
|
|
},
|
|
"labels",
|
|
);
|
|
let rewritten = edits
|
|
.iter()
|
|
.find_map(|e| match e {
|
|
EngineEdit::ReplaceBlock { new_block, .. } => {
|
|
Some(serde_json::to_string(new_block).unwrap())
|
|
}
|
|
_ => None,
|
|
})
|
|
.expect("rename must rewrite the table block");
|
|
assert!(
|
|
rewritten.contains("order.labels[]"),
|
|
"collect marker must survive rename, got {rewritten}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unused_scalar_cells_stay_lazy() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
order_dm(),
|
|
{ "id": "e-gold", "type": "expression", "props": { "data": {
|
|
"key": "order.goldLabel",
|
|
"value": "\"gold\""
|
|
} } },
|
|
{ "id": "e-silver", "type": "expression", "props": { "data": {
|
|
"key": "order.silverLabel",
|
|
"value": "\"silver\""
|
|
} } },
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [ { "id": "i1", "name": "", "field": "order.amount" } ],
|
|
"outputs": [
|
|
{ "id": "o1", "name": "", "field": "order.tags[]" },
|
|
{ "id": "o2", "name": "", "field": "order.tier" }
|
|
],
|
|
"rules": [
|
|
{ "i1": "> 100", "o1": "\"bulk\"", "o2": "order.goldLabel" },
|
|
{ "i1": "", "o1": "\"standard\"", "o2": "order.silverLabel" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
let result = ws
|
|
.evaluate(&request(
|
|
json!({ "order": { "amount": 500, "region": "US", "express": false } }),
|
|
true,
|
|
))
|
|
.expect("evaluate");
|
|
let executed: Vec<String> = result
|
|
.trace
|
|
.expect("trace")
|
|
.executions
|
|
.iter()
|
|
.map(|e| e.block_id.to_string())
|
|
.collect();
|
|
assert!(
|
|
executed.contains(&"e-gold".to_string()),
|
|
"resolving row's tier dependency must run: {executed:?}"
|
|
);
|
|
assert!(
|
|
!executed.contains(&"e-silver".to_string()),
|
|
"non-resolving row's tier dependency must stay lazy: {executed:?}"
|
|
);
|
|
|
|
let output: serde_json::Value = result.output.into();
|
|
assert_eq!(output.pointer("/order/tier"), Some(&json!("gold")));
|
|
assert_eq!(
|
|
output.pointer("/order/tags"),
|
|
Some(&json!(["bulk", "standard"]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn equality_index_matches_linear_semantics() {
|
|
let doc = json!({
|
|
"blocks": [
|
|
{ "id": "dm", "type": "dataModel", "props": { "data": {
|
|
"name": "order",
|
|
"properties": [
|
|
{ "id": "p1", "name": "amount", "type": "number", "array": false, "optional": false },
|
|
{ "id": "p2", "name": "region", "type": "string", "array": false, "optional": false },
|
|
{ "id": "p3", "name": "express", "type": "boolean", "array": false, "optional": false }
|
|
]
|
|
} } },
|
|
{ "id": "dt", "type": "decisionTable", "props": { "data": {
|
|
"inputs": [
|
|
{ "id": "i1", "name": "", "field": "order.region" },
|
|
{ "id": "i2", "name": "", "field": "order.amount" },
|
|
{ "id": "i3", "name": "", "field": "order.express" }
|
|
],
|
|
"outputs": [ { "id": "o1", "name": "", "field": "order.bucket" } ],
|
|
"rules": [
|
|
{ "i1": "\"US\"", "i2": "1", "i3": "true", "o1": "\"r0\"" },
|
|
{ "i1": "\"US\", \"EU\"", "i2": "2", "i3": "", "o1": "\"r1\"" },
|
|
{ "i1": "in [\"EU\"]", "i2": "in [3, 4]", "i3": "false", "o1": "\"r2\"" },
|
|
{ "i1": "(\"US\")", "i2": "[10..20]", "i3": "", "o1": "\"r3\"" },
|
|
{ "i1": "", "i2": "100", "i3": "", "o1": "\"r4\"" },
|
|
{ "i1": "\"AP\"", "i2": "", "i3": "", "o1": "\"r5\"" },
|
|
{ "i1": "startsWith($, \"E\")", "i2": "777", "i3": "", "o1": "\"r6\"" },
|
|
{ "i1": "\"US\"", "i2": "> 1000", "i3": "", "o1": "\"r7\"" },
|
|
{ "i1": "\"EU\"", "i2": "2.50", "i3": "", "o1": "\"r8\"" },
|
|
{ "i1": "", "i2": "", "i3": "", "o1": "\"r9\"" }
|
|
]
|
|
} } }
|
|
]
|
|
});
|
|
let ws = workspace_with(doc);
|
|
let bucket_for =
|
|
|region: &str, amount: serde_json::Value, express: bool| -> serde_json::Value {
|
|
evaluate_output(
|
|
&ws,
|
|
json!({ "order": { "region": region, "amount": amount, "express": express } }),
|
|
)
|
|
.pointer("/order/bucket")
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null)
|
|
};
|
|
|
|
assert_eq!(bucket_for("US", json!(1), true), json!("r0"));
|
|
assert_eq!(bucket_for("US", json!(1), false), json!("r9"));
|
|
assert_eq!(bucket_for("EU", json!(2), false), json!("r1"));
|
|
assert_eq!(bucket_for("EU", json!(3), false), json!("r2"));
|
|
assert_eq!(bucket_for("EU", json!(4), true), json!("r9"));
|
|
assert_eq!(bucket_for("US", json!(15), false), json!("r3"));
|
|
assert_eq!(bucket_for("AP", json!(100), false), json!("r4"));
|
|
assert_eq!(bucket_for("AP", json!(7), false), json!("r5"));
|
|
assert_eq!(bucket_for("EU", json!(777), false), json!("r6"));
|
|
assert_eq!(bucket_for("US", json!(5000), false), json!("r7"));
|
|
assert_eq!(bucket_for("EU", json!(2.5), false), json!("r8"));
|
|
assert_eq!(bucket_for("US", json!(999), false), json!("r9"));
|
|
}
|