Skip to main content

conjure_cp_cli/utils/
testing.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::fmt::Debug;
3use std::path::Path;
4use std::{io, mem, vec};
5
6use conjure_cp::ast::records::Field;
7use conjure_cp::ast::serde::ObjId;
8use itertools::Itertools as _;
9use std::fs::File;
10use std::hash::Hash;
11use std::io::{BufRead, BufReader};
12use std::sync::{Arc, RwLock};
13use uniplate::Uniplate;
14
15use conjure_cp::ast::pretty::pretty_expression_domain_annotation;
16use conjure_cp::ast::{AbstractLiteral, Expression, GroundDomain, Moo, SerdeModel};
17use conjure_cp::context::Context;
18use serde_json::{Error as JsonError, Value as JsonValue};
19
20use conjure_cp::error::Error;
21
22use crate::utils::conjure::solutions_to_essence;
23use crate::utils::json::sort_json_object;
24use crate::utils::misc::to_set;
25use conjure_cp::Model as ConjureModel;
26use conjure_cp::ast::Name::User;
27use conjure_cp::ast::{Literal, Name};
28use conjure_cp::settings::SolverFamily;
29
30/// Limit how many lines of the rewrite serialisation we persist/compare in exhaustive tests.
31pub const REWRITE_SERIALISED_JSON_MAX_LINES: usize = 1000;
32
33/// Limit how many characters we persist/compare for large text snapshots.
34pub const DEFAULT_TEXT_SNAPSHOT_CHARACTER_LIMIT: usize = 1_000_000;
35
36/// Converts a SerdeModel to JSON with stable IDs.
37///
38/// This ensures that the same model structure always produces the same IDs,
39/// regardless of the order in which objects were created in memory.
40fn model_to_json_with_stable_ids(model: &SerdeModel) -> Result<JsonValue, JsonError> {
41    // Collect stable ID mapping using uniplate traversal on the SerdeModel
42    let id_map = model.collect_stable_id_mapping();
43
44    // Serialize the model to JSON
45    let mut json = serde_json::to_value(model)?;
46
47    // Replace all IDs in the JSON with their stable counterparts
48    replace_ids(&mut json, &id_map);
49
50    Ok(json)
51}
52
53/// Recursively replaces all IDs in the JSON with their stable counterparts.
54///
55/// This is applied to all fields that are called "id" or "ptr" - be mindful
56/// of potential naming clashes in the future!
57fn replace_ids(value: &mut JsonValue, id_map: &HashMap<ObjId, ObjId>) {
58    match value {
59        JsonValue::Object(map) => {
60            // Replace IDs in three places:
61            // - "id" fields (SymbolTable IDs)
62            // - "parent" fields (SymbolTable nesting)
63            // - "ptr" fields (DeclarationPtr IDs)
64            for (k, v) in map.iter_mut() {
65                if (k == "id" || k == "ptr" || k == "parent")
66                    && let Ok(old_id) = serde_json::from_value::<ObjId>(mem::take(v))
67                {
68                    let new_id = id_map.get(&old_id).expect("all ids to be in the id map");
69                    *v = serde_json::to_value(new_id)
70                        .expect("serialization of an ObjId to always succeed");
71                }
72            }
73
74            // Recursively process all values
75            for val in map.values_mut() {
76                replace_ids(val, id_map);
77            }
78        }
79        JsonValue::Array(arr) => {
80            for item in arr {
81                replace_ids(item, id_map);
82            }
83        }
84        _ => {}
85    }
86}
87
88pub fn assert_eq_any_order<T: Eq + Hash + Debug + Clone>(a: &Vec<Vec<T>>, b: &Vec<Vec<T>>) {
89    assert_eq!(a.len(), b.len());
90
91    let mut a_rows: Vec<HashSet<T>> = Vec::new();
92    for row in a {
93        let hash_row = to_set(row);
94        a_rows.push(hash_row);
95    }
96
97    let mut b_rows: Vec<HashSet<T>> = Vec::new();
98    for row in b {
99        let hash_row = to_set(row);
100        b_rows.push(hash_row);
101    }
102
103    for row in a_rows {
104        assert!(b_rows.contains(&row));
105    }
106}
107
108pub fn serialize_model(model: &ConjureModel) -> Result<String, JsonError> {
109    let serde_model: SerdeModel = model.clone().into();
110
111    // Convert to JSON with stable IDs
112    let json_with_stable_ids = model_to_json_with_stable_ids(&serde_model)?;
113
114    // Sort JSON object keys for consistent output
115    let sorted_json = sort_json_object(&json_with_stable_ids, false);
116
117    // Serialize to pretty-printed string
118    serde_json::to_string_pretty(&sorted_json)
119}
120
121pub fn serialize_domains(model: &ConjureModel) -> Result<String, JsonError> {
122    let mut output = String::new();
123    for constraint in model.constraints() {
124        serialize_domains_expr(constraint, 0, &mut output);
125    }
126    Ok(output)
127}
128
129fn serialize_domains_expr(expr: &Expression, depth: usize, output: &mut String) {
130    let domain = expr
131        .domain_of()
132        .map(|domain| domain.to_string())
133        .unwrap_or_else(|| "<unknown>".to_owned());
134    output.push_str(&" ".repeat(depth));
135    output.push_str(&pretty_expression_domain_annotation(expr, domain));
136    output.push('\n');
137
138    for child in expr.children() {
139        serialize_domains_expr(&child, depth + 1, output);
140    }
141}
142
143pub fn save_model_json(
144    model: &ConjureModel,
145    path: &str,
146    test_name: &str,
147    test_stage: &str,
148    solver: SolverFamily,
149) -> Result<(), std::io::Error> {
150    let marker = solver.as_str();
151    let generated_json_str = serialize_model(model)?;
152    let generated_json_str = maybe_truncate_serialised_json(generated_json_str, test_stage);
153    let filename = format!("{path}/{test_name}-{marker}.generated-{test_stage}.serialised.json");
154    println!("saving: {filename}");
155    std::fs::write(filename, format!("{generated_json_str}\n"))?;
156    Ok(())
157}
158
159pub fn save_stats_json(
160    context: Arc<RwLock<Context<'static>>>,
161    path: &str,
162    test_name: &str,
163    solver: SolverFamily,
164) -> Result<(), std::io::Error> {
165    #[allow(clippy::unwrap_used)]
166    let solver_name = solver.as_str();
167
168    let stats = context.read().unwrap().clone();
169    let generated_json = sort_json_object(&serde_json::to_value(stats)?, false);
170
171    // serialise to string
172    let generated_json_str = serde_json::to_string_pretty(&generated_json)?;
173
174    std::fs::write(
175        format!("{path}/{test_name}-{solver_name}-stats.json"),
176        format!("{generated_json_str}\n"),
177    )?;
178
179    Ok(())
180}
181
182/// Reads a file into a `String`, providing a clearer error message that includes the file path.
183fn read_with_path(path: String) -> Result<String, std::io::Error> {
184    std::fs::read_to_string(&path)
185        .map_err(|e| io::Error::new(e.kind(), format!("{e} (path: {path})")))
186}
187
188pub fn read_model_json(
189    ctx: &Arc<RwLock<Context<'static>>>,
190    path: &str,
191    test_name: &str,
192    prefix: &str,
193    test_stage: &str,
194    solver: SolverFamily,
195) -> Result<ConjureModel, std::io::Error> {
196    let marker = solver.as_str();
197    let filepath = format!("{path}/{test_name}-{marker}.{prefix}-{test_stage}.serialised.json");
198    let expected_json_str = std::fs::read_to_string(filepath)?;
199    let expected_model: SerdeModel = serde_json::from_str(&expected_json_str)?;
200
201    Ok(expected_model.initialise(ctx.clone()).unwrap())
202}
203
204/// Reads only the first `max_lines` from a serialised model JSON file.
205pub fn read_model_json_prefix(
206    path: &str,
207    test_name: &str,
208    prefix: &str,
209    test_stage: &str,
210    solver: SolverFamily,
211    max_lines: usize,
212) -> Result<String, std::io::Error> {
213    let marker = solver.as_str();
214    let filename = format!("{path}/{test_name}-{marker}.{prefix}-{test_stage}.serialised.json");
215    println!("reading: {filename}");
216    read_first_n_lines(filename, max_lines)
217}
218
219pub fn minion_solutions_from_json(
220    serialized: &str,
221) -> Result<Vec<HashMap<Name, Literal>>, anyhow::Error> {
222    let json: JsonValue = serde_json::from_str(serialized)?;
223
224    let json_array = json
225        .as_array()
226        .ok_or(Error::Parse("Invalid JSON".to_owned()))?;
227
228    let mut solutions = Vec::new();
229
230    for solution in json_array {
231        let mut sol = HashMap::new();
232        let solution = solution
233            .as_object()
234            .ok_or(Error::Parse("Invalid JSON".to_owned()))?;
235
236        for (var_name, constant) in solution {
237            let constant = match constant {
238                JsonValue::Number(n) => {
239                    let n = n
240                        .as_i64()
241                        .ok_or(Error::Parse("Invalid integer".to_owned()))?;
242                    Literal::Int(n as i32)
243                }
244                JsonValue::Bool(b) => Literal::Bool(*b),
245                _ => return Err(Error::Parse("Invalid constant".to_owned()).into()),
246            };
247
248            sol.insert(User(var_name.into()), constant);
249        }
250
251        solutions.push(sol);
252    }
253
254    Ok(solutions)
255}
256
257/// Writes solutions in Conjure's multi-solution Essence format.
258pub fn save_solutions_essence(
259    solutions: &[BTreeMap<Name, Literal>],
260    path: &str,
261    test_name: &str,
262    solver: SolverFamily,
263) -> Result<String, std::io::Error> {
264    let rendered = solutions_to_essence(solutions);
265    let solver_name = solver.as_str();
266    let filename = format!("{path}/{test_name}-{solver_name}.generated.solutions");
267    std::fs::write(filename, &rendered)?;
268
269    Ok(rendered)
270}
271
272pub fn read_solutions_essence(
273    path: &str,
274    test_name: &str,
275    prefix: &str,
276    solver: SolverFamily,
277) -> Result<String, anyhow::Error> {
278    let solver_name = solver.as_str();
279    let filename = format!("{path}/{test_name}-{solver_name}.{prefix}.solutions");
280    Ok(read_with_path(filename)?)
281}
282
283/// Reads a default rule trace text file.
284pub fn read_default_rule_trace(
285    path: &str,
286    test_name: &str,
287    prefix: &str,
288    solver: &SolverFamily,
289) -> Result<String, std::io::Error> {
290    let solver_name = solver.as_str();
291    let filename = format!("{path}/{test_name}-{solver_name}-{prefix}-rule-trace.txt");
292    Ok(truncate_to_first_chars(
293        &read_with_path(filename)?,
294        DEFAULT_TEXT_SNAPSHOT_CHARACTER_LIMIT,
295    ))
296}
297
298#[doc(hidden)]
299pub fn normalize_solutions_for_comparison(
300    input_solutions: &[BTreeMap<Name, Literal>],
301) -> Vec<BTreeMap<Name, Literal>> {
302    let mut normalized = input_solutions.to_vec();
303
304    for solset in &mut normalized {
305        // remove machine names
306        let keys_to_remove: Vec<Name> = solset
307            .keys()
308            .filter(|k| matches!(k, Name::Machine(_)))
309            .cloned()
310            .collect();
311        for k in keys_to_remove {
312            solset.remove(&k);
313        }
314
315        let mut updates = vec![];
316        for (k, v) in solset.clone() {
317            if let Name::User(_) = k {
318                match v {
319                    Literal::Bool(true) => updates.push((k, Literal::Int(1))),
320                    Literal::Bool(false) => updates.push((k, Literal::Int(0))),
321                    Literal::Int(_) | Literal::EnumVariant(_) | Literal::Unnamed(_) => {}
322                    Literal::AbstractLiteral(AbstractLiteral::Matrix(elems, _)) => {
323                        // make all domains the same (this is just in the tester so the types dont
324                        // actually matter)
325
326                        let mut matrix = AbstractLiteral::Matrix(
327                            elems,
328                            Moo::new(GroundDomain::Int(vec![], None)),
329                        );
330                        matrix = matrix.transform(&move |x: AbstractLiteral<Literal>| match x {
331                            AbstractLiteral::Matrix(items, _) => {
332                                let items = items
333                                    .into_iter()
334                                    .map(|x| match x {
335                                        Literal::Bool(false) => Literal::Int(0),
336                                        Literal::Bool(true) => Literal::Int(1),
337                                        x => x,
338                                    })
339                                    .collect_vec();
340
341                                AbstractLiteral::Matrix(
342                                    items,
343                                    Moo::new(GroundDomain::Int(vec![], None)),
344                                )
345                            }
346                            x => x,
347                        });
348                        updates.push((k, Literal::AbstractLiteral(matrix)));
349                    }
350                    Literal::AbstractLiteral(AbstractLiteral::Tuple(elems)) => {
351                        // just the same as matrix but with tuples instead
352                        // only conversion needed is to convert bools to ints
353                        let mut tuple = AbstractLiteral::Tuple(elems);
354                        tuple = tuple.transform(
355                            &(move |x: AbstractLiteral<Literal>| match x {
356                                AbstractLiteral::Tuple(items) => {
357                                    let items = items
358                                        .into_iter()
359                                        .map(|x| match x {
360                                            Literal::Bool(false) => Literal::Int(0),
361                                            Literal::Bool(true) => Literal::Int(1),
362                                            x => x,
363                                        })
364                                        .collect_vec();
365
366                                    AbstractLiteral::Tuple(items)
367                                }
368                                x => x,
369                            }),
370                        );
371                        updates.push((k, Literal::AbstractLiteral(tuple)));
372                    }
373                    Literal::AbstractLiteral(AbstractLiteral::Record(entries)) => {
374                        // just the same as matrix but with tuples instead
375                        // only conversion needed is to convert bools to ints
376                        let mut record = AbstractLiteral::Record(entries);
377                        record = record.transform(&move |x: AbstractLiteral<Literal>| match x {
378                            AbstractLiteral::Record(entries) => {
379                                let entries = entries
380                                    .into_iter()
381                                    .map(|x| {
382                                        let Field { name, value } = x;
383                                        {
384                                            let value = match value {
385                                                Literal::Bool(false) => Literal::Int(0),
386                                                Literal::Bool(true) => Literal::Int(1),
387                                                x => x,
388                                            };
389                                            Field { name, value }
390                                        }
391                                    })
392                                    .collect_vec();
393
394                                AbstractLiteral::Record(entries)
395                            }
396                            x => x,
397                        });
398                        updates.push((k, Literal::AbstractLiteral(record)));
399                    }
400                    Literal::AbstractLiteral(AbstractLiteral::Variant(entry)) => {
401                        let mut variant = AbstractLiteral::Variant(entry);
402                        variant = variant.transform(&move |x| match x {
403                            AbstractLiteral::Variant(entry) => {
404                                let Field { name, value } = Moo::unwrap_or_clone(entry);
405                                let value = match value {
406                                    Literal::Bool(false) => Literal::Int(0),
407                                    Literal::Bool(true) => Literal::Int(1),
408                                    value => value,
409                                };
410                                AbstractLiteral::Variant(Moo::new(Field { name, value }))
411                            }
412                            value => value,
413                        });
414                        updates.push((k, Literal::AbstractLiteral(variant)));
415                    }
416                    Literal::AbstractLiteral(AbstractLiteral::Set(members)) => {
417                        let set = AbstractLiteral::Set(members).transform(&move |x| match x {
418                            AbstractLiteral::Set(members) => {
419                                let members = members
420                                    .into_iter()
421                                    .map(|x| match x {
422                                        Literal::Bool(false) => Literal::Int(0),
423                                        Literal::Bool(true) => Literal::Int(1),
424                                        x => x,
425                                    })
426                                    .collect_vec();
427
428                                AbstractLiteral::Set(members)
429                            }
430                            x => x,
431                        });
432                        updates.push((k, Literal::AbstractLiteral(set)));
433                    }
434                    Literal::AbstractLiteral(AbstractLiteral::MSet(members)) => {
435                        let mset = AbstractLiteral::MSet(members).transform(&move |x| match x {
436                            AbstractLiteral::MSet(members) => {
437                                let members = members
438                                    .into_iter()
439                                    .map(|x| match x {
440                                        Literal::Bool(false) => Literal::Int(0),
441                                        Literal::Bool(true) => Literal::Int(1),
442                                        x => x,
443                                    })
444                                    .collect_vec();
445                                AbstractLiteral::MSet(members)
446                            }
447                            x => x,
448                        });
449                        updates.push((k, Literal::AbstractLiteral(mset)));
450                    }
451                    Literal::AbstractLiteral(AbstractLiteral::Sequence(elems)) => {
452                        let sequence =
453                            AbstractLiteral::Sequence(elems).transform(&move |x| match x {
454                                AbstractLiteral::Sequence(elems) => {
455                                    let elems = elems
456                                        .into_iter()
457                                        .map(|x| match x {
458                                            Literal::Bool(false) => Literal::Int(0),
459                                            Literal::Bool(true) => Literal::Int(1),
460                                            x => x,
461                                        })
462                                        .collect_vec();
463                                    AbstractLiteral::Sequence(elems)
464                                }
465                                x => x,
466                            });
467                        updates.push((k, Literal::AbstractLiteral(sequence)));
468                    }
469                    Literal::AbstractLiteral(AbstractLiteral::Function(pairs)) => {
470                        let function =
471                            AbstractLiteral::Function(pairs).transform(&move |x| match x {
472                                AbstractLiteral::Function(pairs) => {
473                                    let pairs = pairs
474                                        .into_iter()
475                                        .map(|(key, value)| {
476                                            let normalize = |x| match x {
477                                                Literal::Bool(false) => Literal::Int(0),
478                                                Literal::Bool(true) => Literal::Int(1),
479                                                x => x,
480                                            };
481                                            (normalize(key), normalize(value))
482                                        })
483                                        .collect_vec();
484                                    AbstractLiteral::Function(pairs)
485                                }
486                                x => x,
487                            });
488                        updates.push((k, Literal::AbstractLiteral(function)));
489                    }
490                    Literal::AbstractLiteral(AbstractLiteral::Relation(tuples)) => {
491                        let relation =
492                            AbstractLiteral::Relation(tuples).transform(&move |x| match x {
493                                AbstractLiteral::Relation(tuples) => {
494                                    let tuples = tuples
495                                        .into_iter()
496                                        .map(|fields| {
497                                            fields
498                                                .into_iter()
499                                                .map(|x| match x {
500                                                    Literal::Bool(false) => Literal::Int(0),
501                                                    Literal::Bool(true) => Literal::Int(1),
502                                                    x => x,
503                                                })
504                                                .collect_vec()
505                                        })
506                                        .collect_vec();
507                                    AbstractLiteral::Relation(tuples)
508                                }
509                                x => x,
510                            });
511                        updates.push((k, Literal::AbstractLiteral(relation)));
512                    }
513                    Literal::AbstractLiteral(AbstractLiteral::Partition(parts)) => {
514                        let partition =
515                            AbstractLiteral::Partition(parts).transform(&move |x| match x {
516                                AbstractLiteral::Partition(parts) => {
517                                    let parts = parts
518                                        .into_iter()
519                                        .map(|part| {
520                                            part.into_iter()
521                                                .map(|x| match x {
522                                                    Literal::Bool(false) => Literal::Int(0),
523                                                    Literal::Bool(true) => Literal::Int(1),
524                                                    x => x,
525                                                })
526                                                .collect_vec()
527                                        })
528                                        .collect_vec();
529                                    AbstractLiteral::Partition(parts)
530                                }
531                                x => x,
532                            });
533                        updates.push((k, Literal::AbstractLiteral(partition)));
534                    }
535                    Literal::AbstractLiteral(AbstractLiteral::Permutation(cycles)) => {
536                        let permutation =
537                            AbstractLiteral::Permutation(cycles).transform(&move |x| match x {
538                                AbstractLiteral::Permutation(cycles) => {
539                                    let cycles = cycles
540                                        .into_iter()
541                                        .map(|cycle| {
542                                            cycle
543                                                .into_iter()
544                                                .map(|x| match x {
545                                                    Literal::Bool(false) => Literal::Int(0),
546                                                    Literal::Bool(true) => Literal::Int(1),
547                                                    x => x,
548                                                })
549                                                .collect_vec()
550                                        })
551                                        .collect_vec();
552                                    AbstractLiteral::Permutation(cycles)
553                                }
554                                x => x,
555                            });
556                        updates.push((k, Literal::AbstractLiteral(permutation)));
557                    }
558                }
559            }
560        }
561
562        for (k, v) in updates {
563            let v = match v {
564                Literal::AbstractLiteral(value) => {
565                    Literal::AbstractLiteral(normalize_set_literal_order(value))
566                }
567                value => value,
568            };
569            solset.insert(k, v);
570        }
571    }
572
573    // Remove duplicates and put solutions in a stable order for set-equality compares.
574    normalized = normalized.into_iter().unique().collect();
575    normalized.sort_by(solution_essence_cmp);
576    normalized
577}
578
579fn solution_essence_cmp(
580    lhs: &BTreeMap<Name, Literal>,
581    rhs: &BTreeMap<Name, Literal>,
582) -> std::cmp::Ordering {
583    lhs.iter()
584        .zip(rhs)
585        .find_map(|((lhs_name, lhs_value), (rhs_name, rhs_value))| {
586            let ordering = lhs_name.cmp(rhs_name);
587            (ordering != std::cmp::Ordering::Equal)
588                .then_some(ordering)
589                .or_else(|| {
590                    let ordering = lhs_value.essence_cmp(rhs_value);
591                    (ordering != std::cmp::Ordering::Equal).then_some(ordering)
592                })
593        })
594        .unwrap_or_else(|| lhs.len().cmp(&rhs.len()))
595}
596
597fn normalize_set_literal_order(value: AbstractLiteral<Literal>) -> AbstractLiteral<Literal> {
598    value.transform(&|value| match value {
599        AbstractLiteral::Set(mut members) => {
600            members.sort_by(Literal::essence_cmp);
601            AbstractLiteral::Set(members)
602        }
603        AbstractLiteral::MSet(mut members) => {
604            members.sort_by(Literal::essence_cmp);
605            AbstractLiteral::MSet(members)
606        }
607        AbstractLiteral::Function(mut pairs) => {
608            pairs.sort_by(|(k1, _), (k2, _)| Literal::essence_cmp(k1, k2));
609            AbstractLiteral::Function(pairs)
610        }
611        AbstractLiteral::Relation(mut tuples) => {
612            tuples.sort_by(|a, b| {
613                a.iter()
614                    .zip(b.iter())
615                    .map(|(x, y)| Literal::essence_cmp(x, y))
616                    .find(|ord| *ord != std::cmp::Ordering::Equal)
617                    .unwrap_or(std::cmp::Ordering::Equal)
618            });
619            AbstractLiteral::Relation(tuples)
620        }
621        AbstractLiteral::Partition(mut parts) => {
622            for part in parts.iter_mut() {
623                part.sort_by(Literal::essence_cmp);
624            }
625            parts.sort_by(|a, b| {
626                a.iter()
627                    .zip(b.iter())
628                    .map(|(x, y)| Literal::essence_cmp(x, y))
629                    .find(|ord| *ord != std::cmp::Ordering::Equal)
630                    .unwrap_or_else(|| a.len().cmp(&b.len()))
631            });
632            AbstractLiteral::Partition(parts)
633        }
634        value => value,
635    })
636}
637
638fn maybe_truncate_serialised_json(serialised: String, test_stage: &str) -> String {
639    if test_stage == "rewrite" {
640        truncate_to_first_lines(&serialised, REWRITE_SERIALISED_JSON_MAX_LINES)
641    } else {
642        serialised
643    }
644}
645
646fn truncate_to_first_lines(content: &str, max_lines: usize) -> String {
647    content.lines().take(max_lines).join("\n")
648}
649
650pub fn truncate_to_first_chars(content: &str, max_chars: usize) -> String {
651    match content.char_indices().nth(max_chars) {
652        Some((idx, _)) => content[..idx].to_owned(),
653        None => content.to_owned(),
654    }
655}
656
657fn read_first_n_lines<P: AsRef<Path>>(filename: P, n: usize) -> io::Result<String> {
658    let reader = BufReader::new(File::open(&filename)?);
659    let lines = reader
660        .lines()
661        .chunks(n)
662        .into_iter()
663        .next()
664        .unwrap()
665        .collect::<Result<Vec<_>, _>>()?;
666    Ok(lines.join("\n"))
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    fn set(members: Vec<Literal>) -> Literal {
674        Literal::AbstractLiteral(AbstractLiteral::Set(members))
675    }
676
677    #[test]
678    fn solution_normalization_sorts_nested_set_members_by_essence_order() {
679        let inner_one_two = set(vec![Literal::Int(2), Literal::Int(1)]);
680        let inner_two = set(vec![Literal::Int(2)]);
681        let mut oxide_solution = BTreeMap::new();
682        oxide_solution.insert(
683            Name::User("x".into()),
684            set(vec![inner_two.clone(), inner_one_two.clone()]),
685        );
686        let mut conjure_solution = BTreeMap::new();
687        conjure_solution.insert(
688            Name::User("x".into()),
689            set(vec![inner_one_two, inner_two.clone()]),
690        );
691
692        let normalized_oxide = normalize_solutions_for_comparison(&[oxide_solution]);
693        let normalized_conjure = normalize_solutions_for_comparison(&[conjure_solution]);
694        let expected = set(vec![inner_two, set(vec![Literal::Int(1), Literal::Int(2)])]);
695
696        assert_eq!(normalized_oxide, normalized_conjure);
697        assert_eq!(
698            normalized_oxide[0].get(&Name::User("x".into())),
699            Some(&expected)
700        );
701    }
702}