swarm_engine_eval/scenario/
conditions.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9pub struct EvalConditions {
10 #[serde(default)]
12 pub success: Vec<Condition>,
13
14 #[serde(default)]
16 pub failure: Vec<Condition>,
17
18 #[serde(default)]
20 pub on_timeout: TimeoutBehavior,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Condition {
26 pub name: String,
28
29 pub metric: String,
31
32 pub op: CompareOp,
34
35 pub value: ConditionValue,
37}
38
39impl Condition {
40 pub fn new(
42 name: impl Into<String>,
43 metric: impl Into<String>,
44 op: CompareOp,
45 value: impl Into<ConditionValue>,
46 ) -> Self {
47 Self {
48 name: name.into(),
49 metric: metric.into(),
50 op,
51 value: value.into(),
52 }
53 }
54
55 pub fn evaluate(&self, actual: &ConditionValue) -> bool {
57 self.op.compare(actual, &self.value)
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum CompareOp {
65 Eq,
67 Ne,
69 Gt,
71 Gte,
73 Lt,
75 Lte,
77}
78
79impl CompareOp {
80 pub fn compare(&self, actual: &ConditionValue, expected: &ConditionValue) -> bool {
82 match (actual, expected) {
83 (ConditionValue::Integer(a), ConditionValue::Integer(e)) => self.compare_ord(a, e),
84 (ConditionValue::Float(a), ConditionValue::Float(e)) => self.compare_float(*a, *e),
85 (ConditionValue::Integer(a), ConditionValue::Float(e)) => {
86 self.compare_float(*a as f64, *e)
87 }
88 (ConditionValue::Float(a), ConditionValue::Integer(e)) => {
89 self.compare_float(*a, *e as f64)
90 }
91 (ConditionValue::Bool(a), ConditionValue::Bool(e)) => match self {
92 CompareOp::Eq => a == e,
93 CompareOp::Ne => a != e,
94 _ => false,
95 },
96 (ConditionValue::String(a), ConditionValue::String(e)) => match self {
97 CompareOp::Eq => a == e,
98 CompareOp::Ne => a != e,
99 _ => false,
100 },
101 _ => false,
102 }
103 }
104
105 fn compare_ord<T: Ord>(&self, a: &T, e: &T) -> bool {
106 match self {
107 CompareOp::Eq => a == e,
108 CompareOp::Ne => a != e,
109 CompareOp::Gt => a > e,
110 CompareOp::Gte => a >= e,
111 CompareOp::Lt => a < e,
112 CompareOp::Lte => a <= e,
113 }
114 }
115
116 fn compare_float(&self, a: f64, e: f64) -> bool {
117 const EPSILON: f64 = 1e-9;
118 match self {
119 CompareOp::Eq => (a - e).abs() < EPSILON,
120 CompareOp::Ne => (a - e).abs() >= EPSILON,
121 CompareOp::Gt => a > e,
122 CompareOp::Gte => a >= e - EPSILON,
123 CompareOp::Lt => a < e,
124 CompareOp::Lte => a <= e + EPSILON,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(untagged)]
132pub enum ConditionValue {
133 Integer(i64),
134 Float(f64),
135 Bool(bool),
136 String(String),
137}
138
139impl From<i32> for ConditionValue {
140 fn from(v: i32) -> Self {
141 Self::Integer(v as i64)
142 }
143}
144
145impl From<i64> for ConditionValue {
146 fn from(v: i64) -> Self {
147 Self::Integer(v)
148 }
149}
150
151impl From<u64> for ConditionValue {
152 fn from(v: u64) -> Self {
153 Self::Integer(v as i64)
154 }
155}
156
157impl From<f64> for ConditionValue {
158 fn from(v: f64) -> Self {
159 Self::Float(v)
160 }
161}
162
163impl From<bool> for ConditionValue {
164 fn from(v: bool) -> Self {
165 Self::Bool(v)
166 }
167}
168
169impl From<&str> for ConditionValue {
170 fn from(v: &str) -> Self {
171 Self::String(v.to_string())
172 }
173}
174
175impl From<String> for ConditionValue {
176 fn from(v: String) -> Self {
177 Self::String(v)
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum TimeoutBehavior {
185 #[default]
187 Fail,
188 PartialSuccess,
190 MilestoneScore,
192}
193
194#[derive(Debug, Clone)]
196pub struct ConditionResult {
197 pub name: String,
199 pub passed: bool,
201 pub actual: Option<ConditionValue>,
203 pub expected: ConditionValue,
205}
206
207impl ConditionResult {
208 pub fn new(condition: &Condition, actual: Option<ConditionValue>, passed: bool) -> Self {
209 Self {
210 name: condition.name.clone(),
211 passed,
212 actual,
213 expected: condition.value.clone(),
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_compare_op_integer() {
224 let a = ConditionValue::Integer(5);
225 let e = ConditionValue::Integer(3);
226
227 assert!(CompareOp::Gt.compare(&a, &e));
228 assert!(CompareOp::Gte.compare(&a, &e));
229 assert!(!CompareOp::Lt.compare(&a, &e));
230 assert!(!CompareOp::Lte.compare(&a, &e));
231 assert!(!CompareOp::Eq.compare(&a, &e));
232 assert!(CompareOp::Ne.compare(&a, &e));
233 }
234
235 #[test]
236 fn test_compare_op_float() {
237 let a = ConditionValue::Float(0.8);
238 let e = ConditionValue::Float(0.7);
239
240 assert!(CompareOp::Gt.compare(&a, &e));
241 assert!(CompareOp::Gte.compare(&a, &e));
242 }
243
244 #[test]
245 fn test_compare_op_mixed() {
246 let a = ConditionValue::Integer(5);
247 let e = ConditionValue::Float(3.0);
248
249 assert!(CompareOp::Gt.compare(&a, &e));
250 }
251
252 #[test]
253 fn test_condition_evaluate() {
254 let condition = Condition::new("completion_check", "task.completed", CompareOp::Gte, 5);
255
256 assert!(condition.evaluate(&ConditionValue::Integer(5)));
257 assert!(condition.evaluate(&ConditionValue::Integer(10)));
258 assert!(!condition.evaluate(&ConditionValue::Integer(3)));
259 }
260
261 #[test]
262 fn test_condition_deserialize() {
263 let json = r#"{
264 "name": "success_rate",
265 "metric": "task.success_rate",
266 "op": "gte",
267 "value": 0.8
268 }"#;
269
270 let condition: Condition = serde_json::from_str(json).unwrap();
271 assert_eq!(condition.name, "success_rate");
272 assert_eq!(condition.metric, "task.success_rate");
273 assert_eq!(condition.op, CompareOp::Gte);
274 }
275
276 #[test]
277 fn test_timeout_behavior_default() {
278 let behavior = TimeoutBehavior::default();
279 assert_eq!(behavior, TimeoutBehavior::Fail);
280 }
281}