1
use std::collections::HashMap;
2
use std::env;
3
use std::error::Error;
4
use std::fs;
5
use std::path::Path;
6
use std::sync::Arc;
7
use std::sync::Mutex;
8
use std::sync::RwLock;
9

            
10
use conjure_core::ast::Atom;
11
use conjure_core::ast::{Expression, Literal, Name};
12
use conjure_core::context::Context;
13
use conjure_oxide::defaults::get_default_rule_sets;
14
use conjure_oxide::rule_engine::resolve_rule_sets;
15
use conjure_oxide::rule_engine::rewrite_model;
16
use conjure_oxide::utils::conjure::minion_solutions_to_json;
17
use conjure_oxide::utils::conjure::{
18
    get_minion_solutions, get_solutions_from_conjure, parse_essence_file,
19
};
20
use conjure_oxide::utils::testing::save_stats_json;
21
use conjure_oxide::utils::testing::{
22
    read_minion_solutions_json, read_model_json, save_minion_solutions_json, save_model_json,
23
};
24
use conjure_oxide::SolverFamily;
25

            
26
use uniplate::Uniplate;
27

            
28
use serde::Deserialize;
29

            
30
use pretty_assertions::assert_eq;
31

            
32
6
#[derive(Deserialize, Default)]
33
struct TestConfig {
34
    extra_rewriter_asserts: Vec<String>,
35
}
36

            
37
fn main() {
38
    let file_path = Path::new("/path/to/your/file.txt");
39
    let base_name = file_path.file_stem().and_then(|stem| stem.to_str());
40

            
41
    match base_name {
42
        Some(name) => println!("Base name: {}", name),
43
        None => println!("Could not extract the base name"),
44
    }
45
}
46

            
47
// run tests in sequence not parallel when verbose logging, to ensure the logs are ordered
48
// correctly
49
static GUARD: Mutex<()> = Mutex::new(());
50

            
51
// wrapper to conditionally enforce sequential execution
52
50
fn integration_test(path: &str, essence_base: &str, extension: &str) -> Result<(), Box<dyn Error>> {
53
50
    let verbose = env::var("VERBOSE").unwrap_or("false".to_string()) == "true";
54
50

            
55
50
    // run tests in sequence not parallel when verbose logging, to ensure the logs are ordered
56
50
    // correctly
57
50
    if verbose {
58
        #[allow(clippy::unwrap_used)]
59
        #[allow(unused_variables)]
60
        let guard = GUARD.lock().unwrap();
61
        integration_test_inner(path, essence_base, extension)
62
    } else {
63
50
        integration_test_inner(path, essence_base, extension)
64
    }
65
50
}
66

            
67
/// Runs an integration test for a given Conjure model by:
68
/// 1. Parsing the model from an Essence file.
69
/// 2. Rewriting the model according to predefined rule sets.
70
/// 3. Solving the model using the Minion solver and validating the solutions.
71
///
72
/// This function operates in three main stages:
73
/// - **Parsing Stage**: Reads the Essence model file and verifies that it parses correctly.
74
/// - **Rewrite Stage**: Applies a set of rules to the parsed model and validates the result.
75
/// - **Solution Stage**: Uses Minion to solve the model and compares solutions with expected results.
76
///
77
/// # Arguments
78
///
79
/// * `path` - The file path where the Essence model and other resources are located.
80
/// * `essence_base` - The base name of the Essence model file.
81
/// * `extension` - The file extension for the Essence model.
82
///
83
/// # Errors
84
///
85
/// Returns an error if any stage fails due to a mismatch with expected results or file I/O issues.
86
#[allow(clippy::unwrap_used)]
87
50
fn integration_test_inner(
88
50
    path: &str,
89
50
    essence_base: &str,
90
50
    extension: &str,
91
50
) -> Result<(), Box<dyn Error>> {
92
50
    let context: Arc<RwLock<Context<'static>>> = Default::default();
93
50
    let accept = env::var("ACCEPT").unwrap_or("false".to_string()) == "true";
94
50
    let verbose = env::var("VERBOSE").unwrap_or("false".to_string()) == "true";
95
50

            
96
50
    if verbose {
97
        println!(
98
            "Running integration test for {}/{}, ACCEPT={}",
99
            path, essence_base, accept
100
        );
101
50
    }
102

            
103
50
    let config: TestConfig =
104
50
        if let Ok(config_contents) = fs::read_to_string(format!("{}/config.toml", path)) {
105
6
            toml::from_str(&config_contents).unwrap()
106
        } else {
107
44
            Default::default()
108
        };
109

            
110
    // Stage 1: Read the essence file and check that the model is parsed correctly
111
50
    let model = parse_essence_file(path, essence_base, extension, context.clone())?;
112
50
    if verbose {
113
        println!("Parsed model: {:#?}", model)
114
50
    }
115

            
116
50
    context.as_ref().write().unwrap().file_name =
117
50
        Some(format!("{path}/{essence_base}.{extension}"));
118
50

            
119
50
    save_model_json(&model, path, essence_base, "parse", accept)?;
120
50
    let expected_model = read_model_json(path, essence_base, "expected", "parse")?;
121
50
    if verbose {
122
        println!("Expected model: {:#?}", expected_model)
123
50
    }
124

            
125
50
    assert_eq!(model, expected_model);
126

            
127
    // Stage 2: Rewrite the model using the rule engine and check that the result is as expected
128
50
    let rule_sets = resolve_rule_sets(SolverFamily::Minion, &get_default_rule_sets())?;
129
50
    let model = rewrite_model(&model, &rule_sets)?;
130
50
    if verbose {
131
        println!("Rewritten model: {:#?}", model)
132
50
    }
133

            
134
50
    save_model_json(&model, path, essence_base, "rewrite", accept)?;
135

            
136
56
    for extra_assert in config.extra_rewriter_asserts {
137
6
        match extra_assert.as_str() {
138
6
            "vector_operators_have_partially_evaluated" => {
139
6
                assert_vector_operators_have_partially_evaluated(&model)
140
            }
141
            x => println!("Unrecognised extra assert: {}", x),
142
        };
143
    }
144

            
145
50
    let expected_model = read_model_json(path, essence_base, "expected", "rewrite")?;
146
50
    if verbose {
147
        println!("Expected model: {:#?}", expected_model)
148
50
    }
149

            
150
50
    assert_eq!(model, expected_model);
151

            
152
    // Stage 3: Run the model through the Minion solver and check that the solutions are as expected
153
50
    let solutions = get_minion_solutions(model)?;
154

            
155
50
    let solutions_json = save_minion_solutions_json(&solutions, path, essence_base, accept)?;
156
50
    if verbose {
157
        println!("Minion solutions: {:#?}", solutions_json)
158
50
    }
159

            
160
    // test solutions against conjure before writing
161
50
    if accept {
162
        let mut conjure_solutions: Vec<HashMap<Name, Literal>> =
163
            get_solutions_from_conjure(&format!("{}/{}.{}", path, essence_base, extension))?;
164

            
165
        // Change bools to nums in both outputs, as we currently don't convert 0,1 back to
166
        // booleans for Minion.
167

            
168
        // remove machine names from Minion solutions, as the conjure solutions won't have these.
169
        let mut username_solutions = solutions.clone();
170
        for solset in &mut username_solutions {
171
            for (k, v) in solset.clone().into_iter() {
172
                match k {
173
                    conjure_core::ast::Name::MachineName(_) => {
174
                        solset.remove(&k);
175
                    }
176
                    conjure_core::ast::Name::UserName(_) => match v {
177
                        Literal::Bool(true) => {
178
                            solset.insert(k, Literal::Int(1));
179
                        }
180
                        Literal::Bool(false) => {
181
                            solset.insert(k, Literal::Int(0));
182
                        }
183
                        _ => {}
184
                    },
185
                }
186
            }
187
        }
188

            
189
        for solset in &mut conjure_solutions {
190
            for (k, v) in solset.clone().into_iter() {
191
                match v {
192
                    Literal::Bool(true) => {
193
                        solset.insert(k, Literal::Int(1));
194
                    }
195
                    Literal::Bool(false) => {
196
                        solset.insert(k, Literal::Int(0));
197
                    }
198
                    _ => {}
199
                }
200
            }
201
        }
202

            
203
        // I can't make these sets of hashmaps due to hashmaps not implementing hash; so, to
204
        // compare these, I make them both json and compare that.
205

            
206
        let mut conjure_solutions_json: serde_json::Value =
207
            minion_solutions_to_json(&conjure_solutions);
208
        let mut username_solutions_json: serde_json::Value =
209
            minion_solutions_to_json(&username_solutions);
210
        conjure_solutions_json.sort_all_objects();
211
        username_solutions_json.sort_all_objects();
212

            
213
        assert_eq!(
214
            username_solutions_json, conjure_solutions_json,
215
            "Solutions (left) do not match conjure (right)!"
216
        );
217
50
    }
218

            
219
50
    let expected_solutions_json = read_minion_solutions_json(path, essence_base, "expected")?;
220
50
    if verbose {
221
        println!("Expected solutions: {:#?}", expected_solutions_json)
222
50
    }
223

            
224
50
    assert_eq!(solutions_json, expected_solutions_json);
225

            
226
50
    save_stats_json(context, path, essence_base)?;
227

            
228
50
    Ok(())
229
50
}
230

            
231
6
fn assert_vector_operators_have_partially_evaluated(model: &conjure_core::Model) {
232
61
    model.constraints.transform(Arc::new(|x| {
233
        use conjure_core::ast::Expression::*;
234
61
        match &x {
235
            Bubble(_, _, _) => (),
236
24
            Atomic(_, _) => (),
237
            Sum(_, vec) => assert_constants_leq_one(&x, vec),
238
            Min(_, vec) => assert_constants_leq_one(&x, vec),
239
            Max(_, vec) => assert_constants_leq_one(&x, vec),
240
            Not(_, _) => (),
241
7
            Or(_, vec) => assert_constants_leq_one(&x, vec),
242
6
            And(_, vec) => assert_constants_leq_one(&x, vec),
243
2
            Eq(_, _, _) => (),
244
            Neq(_, _, _) => (),
245
            Geq(_, _, _) => (),
246
            Leq(_, _, _) => (),
247
            Gt(_, _, _) => (),
248
            Lt(_, _, _) => (),
249
            SafeDiv(_, _, _) => (),
250
            UnsafeDiv(_, _, _) => (),
251
            SumEq(_, vec, _) => assert_constants_leq_one(&x, vec),
252
2
            SumGeq(_, vec, _) => assert_constants_leq_one(&x, vec),
253
3
            SumLeq(_, vec, _) => assert_constants_leq_one(&x, vec),
254
            DivEqUndefZero(_, _, _, _) => (),
255
2
            Ineq(_, _, _, _) => (),
256
            // this is a vector operation, but we don't want to fold values into each-other in this
257
            // one
258
            AllDiff(_, _) => (),
259
15
            WatchedLiteral(_, _, _) => (),
260
            Reify(_, _, _) => (),
261
            AuxDeclaration(_, _, _) => (),
262
            UnsafeMod(_, _, _) => (),
263
            SafeMod(_, _, _) => (),
264
            ModuloEqUndefZero(_, _, _, _) => (),
265
        };
266
61
        x.clone()
267
61
    }));
268
6
}
269

            
270
18
fn assert_constants_leq_one(parent_expr: &Expression, exprs: &[Expression]) {
271
18
    let count = exprs.iter().fold(0, |i, x| match x {
272
        Expression::Atomic(_, Atom::Literal(_)) => i + 1,
273
40
        _ => i,
274
40
    });
275
18

            
276
18
    assert!(count <= 1, "assert_vector_operators_have_partially_evaluated: expression {} is not partially evaluated",parent_expr)
277
18
}
278

            
279
#[test]
280
1
fn assert_conjure_present() {
281
1
    conjure_oxide::find_conjure::conjure_executable().unwrap();
282
1
}
283

            
284
include!(concat!(env!("OUT_DIR"), "/gen_tests.rs"));