1
use std::collections::{BTreeMap, HashSet};
2
use std::fs;
3
use std::io;
4
use std::path::Path;
5

            
6
use conjure_cp::ast::{AbstractLiteral, GroundDomain, Literal, Name};
7
use conjure_cp_cli::utils::conjure::solutions_to_essence;
8
use uniplate::Uniplate;
9

            
10
/// Use the same value spelling across solvers without discarding matrix index domains.
11
40895
pub fn render_shared_solutions(solutions: &[BTreeMap<Name, Literal>]) -> String {
12
40895
    let mut canonical: Vec<BTreeMap<Name, Literal>> = solutions
13
40895
        .iter()
14
483552
        .map(|solution| {
15
483552
            solution
16
483552
                .iter()
17
1508059
                .map(|(name, value)| {
18
2654502
                    let value = value.transform(&|literal| match literal {
19
150029
                        Literal::Bool(value) => Literal::Int(i32::from(value)),
20
                        Literal::AbstractLiteral(AbstractLiteral::Matrix(values, domain)) => {
21
                            // Some adaptors attach the full matrix domain, others only its index
22
                            // domain. Each nested matrix stores its own remaining index domains.
23
247058
                            let domain = match domain.as_ref() {
24
                                GroundDomain::Matrix(_, indices, _) => {
25
4135
                                    indices.first().cloned().unwrap_or(domain)
26
                                }
27
242923
                                _ => domain,
28
                            };
29
247058
                            Literal::AbstractLiteral(AbstractLiteral::Matrix(values, domain))
30
                        }
31
                        Literal::AbstractLiteral(AbstractLiteral::Set(mut values)) => {
32
52291
                            values.sort_by(Literal::essence_cmp);
33
52291
                            Literal::AbstractLiteral(AbstractLiteral::Set(values))
34
                        }
35
                        Literal::AbstractLiteral(AbstractLiteral::MSet(mut values)) => {
36
17751
                            values.sort_by(Literal::essence_cmp);
37
17751
                            Literal::AbstractLiteral(AbstractLiteral::MSet(values))
38
                        }
39
                        Literal::AbstractLiteral(AbstractLiteral::Record(mut fields)) => {
40
30672
                            fields.sort_by_key(|field| field.name.to_string());
41
15336
                            Literal::AbstractLiteral(AbstractLiteral::Record(fields))
42
                        }
43
                        Literal::AbstractLiteral(AbstractLiteral::Function(mut pairs)) => {
44
5199
                            pairs.sort_by(|(ak, av), (bk, bv)| {
45
5199
                                ak.essence_cmp(bk).then_with(|| av.essence_cmp(bv))
46
5199
                            });
47
4339
                            Literal::AbstractLiteral(AbstractLiteral::Function(pairs))
48
                        }
49
                        Literal::AbstractLiteral(AbstractLiteral::Relation(mut rows)) => {
50
1016
                            rows.sort_by(|a, b| compare_rows(a, b));
51
374
                            Literal::AbstractLiteral(AbstractLiteral::Relation(rows))
52
                        }
53
                        Literal::AbstractLiteral(AbstractLiteral::Partition(mut parts)) => {
54
                            for part in &mut parts {
55
                                part.sort_by(Literal::essence_cmp);
56
                            }
57
                            parts.sort_by(|a, b| compare_rows(a, b));
58
                            Literal::AbstractLiteral(AbstractLiteral::Partition(parts))
59
                        }
60
2167324
                        other => other,
61
2654502
                    });
62
1508059
                    (name.clone(), value)
63
1508059
                })
64
                .collect()
65
483552
        })
66
        .collect();
67
    // Different representations can produce the same assignment more than once.
68
    // Compare sets of problem assignments, independently of auxiliary solver variables.
69
40895
    let mut seen = HashSet::new();
70
483552
    canonical.retain(|solution| seen.insert(solution.clone()));
71
40895
    let rendered = solutions_to_essence(&canonical);
72
40895
    if rendered.is_empty() {
73
897
        rendered
74
    } else {
75
39998
        format!("{}\n", rendered.trim_end())
76
    }
77
40895
}
78

            
79
1016
fn compare_rows(a: &[Literal], b: &[Literal]) -> std::cmp::Ordering {
80
1016
    a.iter()
81
1016
        .zip(b)
82
1434
        .map(|(a, b)| a.essence_cmp(b))
83
1434
        .find(|ordering| *ordering != std::cmp::Ordering::Equal)
84
        .unwrap_or_else(|| a.len().cmp(&b.len()))
85
1016
}
86

            
87
/// Holds one complete solution set while all configurations of a fixture are checked.
88
#[derive(Default)]
89
pub struct SharedSolutionSnapshot {
90
    first: Option<(String, String)>,
91
    disagreement: bool,
92
}
93

            
94
impl SharedSolutionSnapshot {
95
40895
    pub fn record(&mut self, run_label: &str, rendered: String) -> io::Result<()> {
96
40895
        if let Some((first_label, first)) = &self.first {
97
40329
            if first != &rendered {
98
                self.disagreement = true;
99
                return Err(io::Error::other(format!(
100
                    "Complete solution sets disagree between {first_label} and {run_label}; \
101
                     shared solution snapshot was not accepted. See the generated solution files."
102
                )));
103
40329
            }
104
        } else {
105
566
            self.first = Some((run_label.to_owned(), rendered));
106
        }
107
40895
        Ok(())
108
40895
    }
109

            
110
    /// Called only after every run succeeds, including trace and reference validation.
111
591
    pub fn finish(&self, expected_path: &Path, accept: bool) -> io::Result<()> {
112
591
        if self.disagreement {
113
            return Err(io::Error::other(
114
                "Cannot publish a shared snapshot after configurations disagreed",
115
            ));
116
591
        }
117
591
        let Some((run_label, rendered)) = &self.first else {
118
25
            return Ok(());
119
        };
120
566
        if accept {
121
            fs::write(expected_path, rendered)
122
        } else {
123
566
            let expected = fs::read_to_string(expected_path)?;
124
566
            if &expected != rendered {
125
                return Err(io::Error::other(format!(
126
                    "Complete solution set from {run_label} does not match {}. \
127
                     See the generated solution files.",
128
                    expected_path.display()
129
                )));
130
566
            }
131
566
            Ok(())
132
        }
133
591
    }
134
}