Skip to content

Commit 4bee4ec

Browse files
committed
add tests to conform to segment rule requirements + change logic so they pass
1 parent c9b346f commit 4bee4ec

File tree

1 file changed

+67
-14
lines changed

1 file changed

+67
-14
lines changed

src/segment_evaluation.rs

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,39 @@ fn belong_to_segment(segment: &Segment, attrs: HashMap<String, AttrValue>) -> An
8181
let operator = &rule.operator;
8282
let attr_name = &rule.attribute_name;
8383
let attr_value = attrs
84-
.get(attr_name)
85-
.ok_or(Error::Other(format!("Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity")))?;
86-
for value in rule.values.iter()
87-
{
88-
let result = check_operator(attr_value, operator, value).context(format!("Operation '{attr_name}' '{operator}' '{value}' failed to evaluate."))?;
89-
if result {return Ok(true)}
84+
.get(attr_name);
85+
if attr_value.is_none(){
86+
return Ok(false)
87+
}
88+
let rule_result = match attr_value{
89+
None => {
90+
println!("Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity");
91+
Ok(false)
92+
},
93+
Some(attr_value) => {
94+
// FIXME: the following algorithm is too hard to read. Is it just me or do we need to simplify this?
95+
// One of the values needs to match.
96+
// Find a candidate (a candidate corresponds to a value which matches or which might match but the operator failed):
97+
let candidate = rule.values.iter().find_map(|value| {
98+
let result_for_value = check_operator(attr_value, operator, value).context(format!("Operation '{attr_name}' '{operator}' '{value}' failed to evaluate."));
99+
match result_for_value{
100+
Ok(true) => Some(Ok(())),
101+
Ok(false) => None,
102+
Err(e) => Some(Err(e))
103+
}
104+
});
105+
// check if the candidate is good, or if the operator failed:
106+
match candidate{
107+
None => Ok(false),
108+
Some(Ok(())) => Ok(true),
109+
Some(Err(e)) => Err(e)
110+
}
111+
}
112+
}?;
113+
// All rules must match:
114+
if !rule_result {
115+
return Ok(false)
90116
}
91-
return Ok(false);
92117
}
93118
Ok(true)
94119
}
@@ -212,27 +237,55 @@ pub mod tests {
212237
}]
213238
}
214239

240+
// SCENARIO - If the SDK user fail to pass the “attributes” for evaluation of featureflag which is segmented - we have considered that evaluation as “does not belong to any segment” and we serve the enabled_value.
241+
// EXAMPLE - Assume two teams are using same featureflag. One team is interested only in enabled_value & disabled_value. This team doesn’t pass attributes for their evaluation. Other team wants to have overridden_value, as a result they update the featureflag by adding segment rules to it. This team passes attributes in their evaluation to get the overridden_value for matching segment, and enabled_value for non-matching segment.
242+
// We should not fail the evaluation.
215243
#[rstest]
216244
fn test_attribute_not_found(segments: HashMap<String, Segment>, segment_rules: Vec<TargetingRule>) {
217245
let entity = crate::tests::GenericEntity {
218246
id: "a2".into(),
219247
attributes: HashMap::from([("name2".into(), AttrValue::from("heinz".to_string()))]),
220248
};
221249
let rule =
222-
find_applicable_segment_rule_for_entity(&segments, segment_rules.clone().into_iter(), &entity);
250+
find_applicable_segment_rule_for_entity(&segments, segment_rules.into_iter(), &entity);
251+
// Segment evaluation should not fail:
252+
let rule = rule.unwrap();
253+
// But no segment should be found:
254+
assert!(rule.is_none())
255+
}
256+
257+
// SCENARIO - The segment_id present in featureflag is invalid. In other words - the /config json dump has a featureflag, which has segment_rules. The segment_id in this segment_rules is invalid. Because this segment_id is not found in segments array.
258+
// This is a very good question. Firstly, the our server-side API are strongly validating inputs and give the responses. We have unittests & integration tests that verifies the input & output of /config API. The response is always right. It is very much rare scenario where the API response has segment_id in featureflag object, that is not present is segments array.
259+
// We can agree to return error and mark evaluation as failed.
260+
#[rstest]
261+
fn test_invalid_segment_id(segments: HashMap<String, Segment>) {
262+
let entity = crate::tests::GenericEntity {
263+
id: "a2".into(),
264+
attributes: HashMap::from([("name".into(), AttrValue::from(42.0))]),
265+
};
266+
let segment_rules = vec![TargetingRule {
267+
rules: vec![Segments {
268+
segments: vec!["non_existing_segment_id".into()],
269+
}],
270+
value: ConfigValue(serde_json::Value::Number((-48).into())),
271+
order: 0,
272+
rollout_percentage: Some(ConfigValue(serde_json::Value::Number((100).into()))),
273+
}];
274+
let rule =
275+
find_applicable_segment_rule_for_entity(&segments, segment_rules.into_iter(), &entity);
223276
// Error message should look something like this:
224-
// Failed to evaluate entity 'a2' against targeting rule '0'.
225-
// Caused by: Failed to evaluate segment 'some_segment_id_1'
226-
// Caused by: Operation 'name' 'is' '[...]' failed to evaluate: 'name' not found in entity
277+
// Failed to evaluate entity: Failed to evaluate entity 'a2' against targeting rule '0'.
278+
// Caused by: Segment 'non_existing_segment_id' not found.
227279
// We are checking here that the parts are present to allow debugging of config by the user:
228280
let msg = rule.unwrap_err().to_string();
229-
assert!(msg.contains("'name' not found in entity"));
230281
assert!(msg.contains("'a2'"));
231282
assert!(msg.contains("'0'"));
232-
assert!(msg.contains("'some_segment_id_1'"));
233-
assert!(msg.contains("'is'"));
283+
assert!(msg.contains("'non_existing_segment_id'"));
284+
assert!(msg.contains("not found"));
234285
}
235286

287+
// SCENARIO - evaluating an operator fails. Meaning, [for example] user has added a numeric value(int/float) in appconfig segment attribute, but in their application they pass the attribute with a boolean value.
288+
// We can mark this as failure and return error.
236289
#[rstest]
237290
fn test_operator_failed(segments: HashMap<String, Segment>, segment_rules: Vec<TargetingRule>) {
238291
let entity = crate::tests::GenericEntity {

0 commit comments

Comments
 (0)