conjure_oxide/utils/
testing.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::fmt::Debug;
3
4use std::fs::File;
5use std::fs::{read_to_string, OpenOptions};
6use std::hash::Hash;
7use std::io::Write;
8use std::sync::{Arc, RwLock};
9
10use conjure_core::ast::SerdeModel;
11use conjure_core::context::Context;
12use serde_json::{json, Error as JsonError, Value as JsonValue};
13
14use conjure_core::error::Error;
15
16use crate::ast::Name::UserName;
17use crate::ast::{Literal, Name};
18use crate::utils::conjure::minion_solutions_to_json;
19use crate::utils::json::sort_json_object;
20use crate::utils::misc::to_set;
21use crate::Model as ConjureModel;
22
23pub fn assert_eq_any_order<T: Eq + Hash + Debug + Clone>(a: &Vec<Vec<T>>, b: &Vec<Vec<T>>) {
24    assert_eq!(a.len(), b.len());
25
26    let mut a_rows: Vec<HashSet<T>> = Vec::new();
27    for row in a {
28        let hash_row = to_set(row);
29        a_rows.push(hash_row);
30    }
31
32    let mut b_rows: Vec<HashSet<T>> = Vec::new();
33    for row in b {
34        let hash_row = to_set(row);
35        b_rows.push(hash_row);
36    }
37
38    println!("{:?},{:?}", a_rows, b_rows);
39    for row in a_rows {
40        assert!(b_rows.contains(&row));
41    }
42}
43
44pub fn serialise_model(model: &ConjureModel) -> Result<String, JsonError> {
45    // A consistent sorting of the keys of json objects
46    // only required for the generated version
47    // since the expected version will already be sorted
48    let serde_model: SerdeModel = model.clone().into();
49    let generated_json = sort_json_object(&serde_json::to_value(serde_model)?, false);
50
51    // serialise to string
52    let generated_json_str = serde_json::to_string_pretty(&generated_json)?;
53
54    Ok(generated_json_str)
55}
56
57pub fn save_model_json(
58    model: &ConjureModel,
59    path: &str,
60    test_name: &str,
61    test_stage: &str,
62) -> Result<(), std::io::Error> {
63    let generated_json_str = serialise_model(model)?;
64    let filename = format!("{path}/{test_name}.generated-{test_stage}.serialised.json");
65    File::create(&filename)?.write_all(generated_json_str.as_bytes())?;
66    Ok(())
67}
68
69pub fn save_stats_json(
70    context: Arc<RwLock<Context<'static>>>,
71    path: &str,
72    test_name: &str,
73) -> Result<(), std::io::Error> {
74    #[allow(clippy::unwrap_used)]
75    let stats = context.read().unwrap().clone();
76    let generated_json = sort_json_object(&serde_json::to_value(stats)?, false);
77
78    // serialise to string
79    let generated_json_str = serde_json::to_string_pretty(&generated_json)?;
80
81    File::create(format!("{path}/{test_name}-stats.json"))?
82        .write_all(generated_json_str.as_bytes())?;
83
84    Ok(())
85}
86
87pub fn read_model_json(
88    ctx: &Arc<RwLock<Context<'static>>>,
89    path: &str,
90    test_name: &str,
91    prefix: &str,
92    test_stage: &str,
93) -> Result<ConjureModel, std::io::Error> {
94    let expected_json_str = std::fs::read_to_string(format!(
95        "{path}/{test_name}.{prefix}-{test_stage}.serialised.json"
96    ))?;
97    println!("{path}/{test_name}.{prefix}-{test_stage}.serialised.json");
98    let expected_model: SerdeModel = serde_json::from_str(&expected_json_str)?;
99
100    Ok(expected_model.initialise(ctx.clone()).unwrap())
101}
102
103pub fn minion_solutions_from_json(
104    serialized: &str,
105) -> Result<Vec<HashMap<Name, Literal>>, anyhow::Error> {
106    let json: JsonValue = serde_json::from_str(serialized)?;
107
108    let json_array = json
109        .as_array()
110        .ok_or(Error::Parse("Invalid JSON".to_owned()))?;
111
112    let mut solutions = Vec::new();
113
114    for solution in json_array {
115        let mut sol = HashMap::new();
116        let solution = solution
117            .as_object()
118            .ok_or(Error::Parse("Invalid JSON".to_owned()))?;
119
120        for (var_name, constant) in solution {
121            let constant = match constant {
122                JsonValue::Number(n) => {
123                    let n = n
124                        .as_i64()
125                        .ok_or(Error::Parse("Invalid integer".to_owned()))?;
126                    Literal::Int(n as i32)
127                }
128                JsonValue::Bool(b) => Literal::Bool(*b),
129                _ => return Err(Error::Parse("Invalid constant".to_owned()).into()),
130            };
131
132            sol.insert(UserName(var_name.into()), constant);
133        }
134
135        solutions.push(sol);
136    }
137
138    Ok(solutions)
139}
140
141/// Writes the minion solutions to a generated JSON file, and returns the JSON structure.
142pub fn save_minion_solutions_json(
143    solutions: &Vec<BTreeMap<Name, Literal>>,
144    path: &str,
145    test_name: &str,
146) -> Result<JsonValue, std::io::Error> {
147    let json_solutions = minion_solutions_to_json(solutions);
148    let generated_json_str = serde_json::to_string_pretty(&json_solutions)?;
149
150    let filename = format!("{path}/{test_name}.generated-minion.solutions.json");
151    File::create(&filename)?.write_all(generated_json_str.as_bytes())?;
152
153    Ok(json_solutions)
154}
155
156pub fn read_minion_solutions_json(
157    path: &str,
158    test_name: &str,
159    prefix: &str,
160) -> Result<JsonValue, anyhow::Error> {
161    let expected_json_str =
162        std::fs::read_to_string(format!("{path}/{test_name}.{prefix}-minion.solutions.json"))?;
163
164    let expected_solutions: JsonValue =
165        sort_json_object(&serde_json::from_str(&expected_json_str)?, true);
166
167    Ok(expected_solutions)
168}
169
170/// Reads a rule trace from a file. For the generated prefix, it appends a count message.
171/// Returns the lines of the file as a vector of strings.
172pub fn read_rule_trace(
173    path: &str,
174    test_name: &str,
175    prefix: &str,
176) -> Result<Vec<String>, std::io::Error> {
177    let filename = format!("{path}/{test_name}-{prefix}-rule-trace.json");
178    let mut rules_trace: Vec<String> = read_to_string(&filename)?
179        .lines()
180        .map(String::from)
181        .collect();
182
183    // If prefix is "generated", append the count message
184    if prefix == "generated" {
185        let rule_count = rules_trace.len();
186        let count_message = json!({
187            "message": "Number of rules applied",
188            "count": rule_count
189        });
190        let count_message_string = serde_json::to_string(&count_message)?;
191        rules_trace.push(count_message_string);
192
193        // Overwrite the file with updated content (including the count message)
194        let mut file = OpenOptions::new()
195            .write(true)
196            .truncate(true)
197            .open(&filename)?;
198        writeln!(file, "{}", rules_trace.join("\n"))?;
199    }
200
201    Ok(rules_trace)
202}
203
204/// Reads a human-readable rule trace text file.
205pub fn read_human_rule_trace(
206    path: &str,
207    test_name: &str,
208    prefix: &str,
209) -> Result<Vec<String>, std::io::Error> {
210    let filename = format!("{path}/{test_name}-{prefix}-rule-trace-human.txt");
211    let rules_trace: Vec<String> = read_to_string(&filename)?
212        .lines()
213        .map(String::from)
214        .collect();
215
216    Ok(rules_trace)
217}