1use crate::{
2 driver::{Config, Path, nondet::NondetPicks},
3 value::ValueDisplay,
4};
5use anyhow::{Context, Result, anyhow, bail};
6use itf::value::{Record, Value};
7use serde::Deserialize;
8use std::fmt;
9
10pub struct Step {
16 #[doc(hidden)] pub action_taken: String,
18 #[doc(hidden)] pub nondet_picks: NondetPicks,
20 pub(crate) state: Value,
21}
22
23impl Step {
24 pub(crate) fn new(state: Record, config: &Config) -> Result<Self> {
25 if config.nondet.is_empty() {
26 extract_from_mbt_vars(state, config.state)
27 } else {
28 extract_from_sum_type(state, config.nondet, config.state)
29 }
30 }
31}
32
33fn extract_from_mbt_vars(mut state: Record, state_path: Path) -> Result<Step> {
34 Ok(Step {
35 action_taken: extract_action_from_mbt_var(&mut state)?,
36 nondet_picks: extract_nondet_from_mbt_var(&mut state)?,
37 state: extract_value_in_path(state, state_path)?,
38 })
39}
40
41fn extract_from_sum_type(mut state: Record, sum_type_path: Path, state_path: Path) -> Result<Step> {
42 let sum_type = find_record_in_path(&state, sum_type_path)?;
43 let action_taken = extract_action_from_sum_type(sum_type)?;
44 let nondet_picks = extract_nondet_from_sum_type(sum_type)?;
45
46 let _ = state.remove("mbt::actionTaken");
48 let _ = state.remove("mbt::nondetPicks");
49
50 let state = extract_value_in_path(state, state_path)?;
51
52 Ok(Step {
53 action_taken,
54 nondet_picks,
55 state,
56 })
57}
58
59fn extract_action_from_mbt_var(state: &mut Record) -> Result<String> {
60 state
61 .remove("mbt::actionTaken")
62 .ok_or(anyhow!("Missing `mbt::actionTaken` variable in the trace"))
63 .and_then(|value| {
64 String::deserialize(value).context("Failed to decode `mbt::actionTaken` variable")
65 })
66}
67
68fn extract_nondet_from_mbt_var(state: &mut Record) -> Result<NondetPicks> {
69 state
70 .remove("mbt::nondetPicks")
71 .ok_or(anyhow!("Missing `mbt::nondetPicks` variable in the trace"))
72 .and_then(|value| {
73 NondetPicks::new(value).context("Failed to extract nondet picks from trace")
74 })
75}
76
77fn extract_value_in_path(state: Record, path: &[&str]) -> Result<Value> {
78 let mut value = Value::Record(state);
79 for segment in path {
80 let Value::Record(mut rec) = value else {
81 bail!(
82 "Can not read {:?} from non-record value in path: {:?}\n\
83 Current value: {}",
84 segment,
85 path,
86 value.display(),
87 )
88 };
89 let Some(next) = rec.remove(segment) else {
90 bail!(
91 "Can not find a value at {:?} in path: {:?}\n\
92 Current value: {}",
93 segment,
94 path,
95 Value::Record(rec).display()
96 )
97 };
98 value = next
99 }
100 Ok(value)
101}
102
103fn find_record_in_path<'a>(state: &'a Record, path: &[&str]) -> Result<&'a Record> {
104 let mut rec = state;
105 for segment in path {
106 let Some(Value::Record(next)) = rec.get(segment) else {
107 bail!(
108 "Can not find a Record at {:?} in path: {:?}\n\
109 Current state: {}",
110 segment,
111 path,
112 state.display()
113 )
114 };
115 rec = next;
116 }
117 Ok(rec)
118}
119
120fn extract_action_from_sum_type(ty: &Record) -> Result<String> {
121 let Some(Value::String(action)) = ty.get("tag") else {
122 bail!(
123 "Expected action to be a sum type variant.\n\
124 Value found: {}",
125 ty.display()
126 )
127 };
128 Ok(action.clone())
129}
130
131fn extract_nondet_from_sum_type(ty: &Record) -> Result<NondetPicks> {
132 match ty.get("value") {
133 Some(Value::Tuple(t)) if t.is_empty() => Ok(NondetPicks::empty()),
134 Some(Value::Record(rec)) => Ok(rec.clone().into()),
135 _ => bail!(
136 "Expected nondet picks to be a sum type variant value as a record.\n\
137 Value found: {}",
138 ty.display()
139 ),
140 }
141}
142
143impl fmt::Display for Step {
144 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145 write!(f, "Action taken:")?;
146 if self.action_taken.is_empty() {
147 writeln!(f, " <anonymous>")?;
148 } else {
149 writeln!(f, " {}", self.action_taken)?;
150 }
151
152 write!(f, "Nondet picks:")?;
153 if self.nondet_picks.is_empty() {
154 writeln!(f, " <none>")?;
155 } else {
156 writeln!(f, "\n{}", self.nondet_picks)?;
157 }
158
159 write!(f, "Next state:")?;
160 match &self.state {
161 Value::Record(rec) => {
162 if rec.is_empty() {
163 write!(f, " <none>")?;
164 } else {
165 for (key, value) in rec.iter() {
166 write!(f, "\n+ {}: {}", key, value.display())?;
167 }
168 }
169 }
170 Value::Map(map) => {
171 if map.is_empty() {
172 write!(f, " <none>")?;
173 } else {
174 for (key, value) in map.iter() {
175 write!(f, "\n+ {}: {}", key.display(), value.display())?;
176 }
177 }
178 }
179 other => write!(f, " {}", other.display())?,
180 }
181 Ok(())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use itf::Value;
189
190 #[test]
191 fn test_extract_value_in_path_empty_path() {
192 let mut rec = Record::new();
193 rec.insert("key".to_string(), Value::String("value".to_string()));
194
195 let result = extract_value_in_path(rec.clone(), &[]).unwrap();
196 assert_eq!(result, Value::Record(rec));
197 }
198
199 #[test]
200 fn test_extract_value_in_path_single_level() {
201 let mut rec = Record::new();
202 rec.insert("key".to_string(), Value::String("value".to_string()));
203
204 let result = extract_value_in_path(rec, &["key"]).unwrap();
205 assert_eq!(result, Value::String("value".to_string()));
206 }
207
208 #[test]
209 fn test_extract_value_in_path_nested() {
210 let mut inner = Record::new();
211 inner.insert("inner_key".to_string(), Value::Number(42));
212
213 let mut outer = Record::new();
214 outer.insert("outer_key".to_string(), Value::Record(inner));
215
216 let result = extract_value_in_path(outer, &["outer_key", "inner_key"]).unwrap();
217 assert_eq!(result, Value::Number(42));
218 }
219
220 #[test]
221 #[should_panic(expected = "Can not find a value at")]
222 fn test_extract_value_in_path_missing_key() {
223 let mut rec = Record::new();
224 rec.insert("key".to_string(), Value::String("value".to_string()));
225
226 extract_value_in_path(rec, &["missing"]).unwrap();
227 }
228
229 #[test]
230 #[should_panic(expected = "non-record value in path")]
231 fn test_extract_value_in_path_non_record() {
232 let mut rec = Record::new();
233 rec.insert("key".to_string(), Value::String("value".to_string()));
234
235 extract_value_in_path(rec, &["key", "nested"]).unwrap();
236 }
237
238 #[test]
239 fn test_find_record_in_path_empty_path() {
240 let rec = Record::new();
241 let result = find_record_in_path(&rec, &[]).unwrap();
242 assert_eq!(result, &rec);
243 }
244
245 #[test]
246 fn test_find_record_in_path_single_level() {
247 let inner = Record::new();
248 let mut outer = Record::new();
249 outer.insert("inner".to_string(), Value::Record(inner.clone()));
250
251 let result = find_record_in_path(&outer, &["inner"]).unwrap();
252 assert_eq!(result, &inner);
253 }
254
255 #[test]
256 fn test_find_record_in_path_nested() {
257 let innermost = Record::new();
258 let mut middle = Record::new();
259 middle.insert("innermost".to_string(), Value::Record(innermost.clone()));
260
261 let mut outer = Record::new();
262 outer.insert("middle".to_string(), Value::Record(middle));
263
264 let result = find_record_in_path(&outer, &["middle", "innermost"]).unwrap();
265 assert_eq!(result, &innermost);
266 }
267
268 #[test]
269 #[should_panic(expected = "Can not find a Record")]
270 fn test_find_record_in_path_missing_key() {
271 let rec = Record::new();
272 find_record_in_path(&rec, &["missing"]).unwrap();
273 }
274
275 #[test]
276 #[should_panic(expected = "Can not find a Record")]
277 fn test_find_record_in_path_non_record() {
278 let mut rec = Record::new();
279 rec.insert("key".to_string(), Value::String("value".to_string()));
280
281 find_record_in_path(&rec, &["key"]).unwrap();
282 }
283
284 #[test]
285 fn test_extract_action_from_mbt_var_success() {
286 let mut rec = Record::new();
287 rec.insert(
288 "mbt::actionTaken".to_string(),
289 Value::String("TestAction".to_string()),
290 );
291
292 let result = extract_action_from_mbt_var(&mut rec).unwrap();
293 assert_eq!(result, "TestAction");
294 assert!(!rec.contains_key("mbt::actionTaken"));
295 }
296
297 #[test]
298 #[should_panic(expected = "Missing `mbt::actionTaken`")]
299 fn test_extract_action_from_mbt_var_missing() {
300 let mut rec = Record::new();
301 extract_action_from_mbt_var(&mut rec).unwrap();
302 }
303
304 #[test]
305 #[should_panic(expected = "Failed to decode `mbt::actionTaken`")]
306 fn test_extract_action_from_mbt_var_wrong_type() {
307 let mut rec = Record::new();
308 rec.insert("mbt::actionTaken".to_string(), Value::Number(42));
309
310 extract_action_from_mbt_var(&mut rec).unwrap();
311 }
312
313 #[test]
314 fn test_extract_nondet_from_mbt_var_success() {
315 let mut rec = Record::new();
316 let nondet_rec = Record::new();
317 rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
318
319 let result = extract_nondet_from_mbt_var(&mut rec);
320 assert!(result.is_ok());
321 assert!(!rec.contains_key("mbt::nondetPicks"));
322 }
323
324 #[test]
325 #[should_panic(expected = "Missing `mbt::nondetPicks`")]
326 fn test_extract_nondet_from_mbt_var_missing() {
327 let mut rec = Record::new();
328 extract_nondet_from_mbt_var(&mut rec).unwrap();
329 }
330
331 #[test]
332 fn test_extract_action_from_sum_type_success() {
333 let mut rec = Record::new();
334 rec.insert("tag".to_string(), Value::String("ActionName".to_string()));
335
336 let result = extract_action_from_sum_type(&rec).unwrap();
337 assert_eq!(result, "ActionName");
338 }
339
340 #[test]
341 #[should_panic(expected = "Expected action to be a sum type variant.")]
342 fn test_extract_action_from_sum_type_missing_tag() {
343 let rec = Record::new();
344 extract_action_from_sum_type(&rec).unwrap();
345 }
346
347 #[test]
348 #[should_panic(expected = "Expected action to be a sum type variant.")]
349 fn test_extract_action_from_sum_type_wrong_type() {
350 let mut rec = Record::new();
351 rec.insert("tag".to_string(), Value::Number(42));
352
353 extract_action_from_sum_type(&rec).unwrap();
354 }
355
356 #[test]
357 fn test_extract_nondet_from_sum_type_empty_tuple() {
358 let mut rec = Record::new();
359 rec.insert("value".to_string(), Value::Tuple(vec![].into()));
360
361 let result = extract_nondet_from_sum_type(&rec).unwrap();
362 assert!(result.is_empty());
363 }
364
365 #[test]
366 fn test_extract_nondet_from_sum_type_record() {
367 let nondet_rec = Record::new();
368 let mut rec = Record::new();
369 rec.insert("value".to_string(), Value::Record(nondet_rec));
370
371 let result = extract_nondet_from_sum_type(&rec);
372 assert!(result.is_ok());
373 }
374
375 #[test]
376 #[should_panic(expected = "Expected nondet picks to be a sum type variant value as a record.")]
377 fn test_extract_nondet_from_sum_type_invalid() {
378 let mut rec = Record::new();
379 rec.insert("value".to_string(), Value::String("invalid".to_string()));
380
381 extract_nondet_from_sum_type(&rec).unwrap();
382 }
383
384 #[test]
385 fn test_extract_from_mbt_vars_success() {
386 let mut rec = Record::new();
387 rec.insert(
388 "mbt::actionTaken".to_string(),
389 Value::String("TestAction".to_string()),
390 );
391
392 let mut nondet_rec = Record::new();
393 nondet_rec.insert("pick1".to_string(), Value::Number(1));
394 rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
395
396 rec.insert(
397 "state_var".to_string(),
398 Value::String("state_value".to_string()),
399 );
400
401 let result = extract_from_mbt_vars(rec, &["state_var"]).unwrap();
402 assert_eq!(result.action_taken, "TestAction");
403 assert_eq!(result.state, Value::String("state_value".to_string()));
404 }
405
406 #[test]
407 #[should_panic(expected = "Missing `mbt::actionTaken`")]
408 fn test_extract_from_mbt_vars_missing_action() {
409 let mut rec = Record::new();
410 let nondet_rec = Record::new();
411 rec.insert("mbt::nondetPicks".to_string(), Value::Record(nondet_rec));
412
413 extract_from_mbt_vars(rec, &[]).unwrap();
414 }
415
416 #[test]
417 fn test_extract_from_sum_type_success() {
418 let mut sum_rec = Record::new();
419 sum_rec.insert("tag".to_string(), Value::String("TestAction".to_string()));
420 sum_rec.insert("value".to_string(), Value::Tuple(vec![].into()));
421
422 let mut rec = Record::new();
423 rec.insert("sum_type".to_string(), Value::Record(sum_rec));
424 rec.insert(
425 "state_var".to_string(),
426 Value::String("state_value".to_string()),
427 );
428
429 let result = extract_from_sum_type(rec, &["sum_type"], &["state_var"]).unwrap();
430 assert_eq!(result.action_taken, "TestAction");
431 assert_eq!(result.state, Value::String("state_value".to_string()));
432 assert!(result.nondet_picks.is_empty());
433 }
434
435 #[test]
436 fn test_extract_from_sum_type_removes_mbt_vars() {
437 let mut sum_rec = Record::new();
438 sum_rec.insert("tag".to_string(), Value::String("TestAction".to_string()));
439 sum_rec.insert("value".to_string(), Value::Tuple(vec![].into()));
440
441 let mut rec = Record::new();
442 rec.insert("sum_type".to_string(), Value::Record(sum_rec));
443 rec.insert(
444 "mbt::actionTaken".to_string(),
445 Value::String("OldAction".to_string()),
446 );
447 rec.insert("mbt::nondetPicks".to_string(), Value::Record(Record::new()));
448 rec.insert(
449 "state_var".to_string(),
450 Value::String("state_value".to_string()),
451 );
452
453 let result = extract_from_sum_type(rec, &["sum_type"], &["state_var"]).unwrap();
454 assert_eq!(result.action_taken, "TestAction");
455 }
456}