1
use conjure_cp::Model;
2
use conjure_cp::context::Context;
3
use conjure_cp::parse::tree_sitter::EssenceParseError;
4
use conjure_cp::parse::tree_sitter::{parse_essence_file, parse_essence_file_native};
5
use conjure_cp_cli::utils::testing::{read_model_json, save_model_json};
6

            
7
use std::env;
8
use std::error::Error;
9
use std::fs;
10
use std::path::Path;
11
use std::sync::Arc;
12
use std::sync::RwLock;
13

            
14
use serde::Deserialize;
15

            
16
use std::io::Write;
17

            
18
// Allows for different configurations of parsers per test
19
#[derive(Deserialize)]
20
struct TestConfig {
21
    parsers: Vec<String>,
22
}
23

            
24
// The default test configuration is both enabled
25
impl Default for TestConfig {
26
    fn default() -> Self {
27
        Self {
28
            parsers: vec![format!("legacy"), format!("native")],
29
        }
30
    }
31
}
32

            
33
// Designed to test if an Essence feature can be parsed correctly into the AST and complete a roundtrip
34
// Does not consider rewriting or solving
35
fn roundtrip_test(path: &str, filename: &str, extension: &str) -> Result<(), Box<dyn Error>> {
36
    // Reads in a config.toml in the test directory
37
    let file_config: TestConfig =
38
        if let Ok(config_contents) = fs::read_to_string(format!("{path}/config.toml")) {
39
            toml::from_str(&config_contents).unwrap()
40
        } else {
41
            Default::default()
42
        };
43
    // Runs native parser
44
    if file_config.parsers.contains(&format!("native")) {
45
        let new_filename = filename.to_owned() + "-native";
46
        roundtrip_test_inner(
47
            path,
48
            &filename,
49
            &new_filename,
50
            extension,
51
            parse_essence_file_native,
52
        )?;
53
    }
54
    // Runs legacy Conjure parser
55
    if file_config.parsers.contains(&format!("legacy")) {
56
        let new_filename = filename.to_owned() + "-legacy";
57
        roundtrip_test_inner(
58
            path,
59
            &filename,
60
            &new_filename,
61
            extension,
62
            parse_essence_file,
63
        )?;
64
    }
65
    Ok(())
66
}
67

            
68
// Runs the test for either parser
69
fn roundtrip_test_inner(
70
    path: &str,
71
    input_filename: &str,
72
    output_filename: &str,
73
    extension: &str,
74
    parse: fn(&str, Arc<RwLock<Context<'static>>>) -> Result<Model, EssenceParseError>,
75
) -> Result<(), Box<dyn Error>> {
76
    /*
77
    Parses Essence file
78
     | If valid
79
        Saves generated AST model JSON
80
        Saves generated Essence
81

            
82
        Compares expected and generated AST model JSON
83
        Compares expected and generated Essence
84

            
85
        Parses generated Essence back to being a model
86
        Saves new model as Essence (generated2)
87
        Compare initally generated Essence with newly generated Essence
88

            
89
    | If invalid
90
        Saves EssenceParseError
91
        Compares expected and generated errors
92
    */
93

            
94
    let accept = env::var("ACCEPT").unwrap_or("false".to_string()) == "true";
95

            
96
    let file_path = format!("{path}/{input_filename}.{extension}");
97
    let context: Arc<RwLock<Context<'static>>> = Default::default();
98

            
99
    let initial_parse = parse(&file_path, context.clone());
100
    match initial_parse {
101
        Ok(initial_model) => {
102
            save_model_json(&initial_model, path, output_filename, "parse")?;
103
            save_essence(&initial_model, path, output_filename, "generated")?;
104

            
105
            // When ACCEPT = true, copy over generated to expected
106
            if accept {
107
                std::fs::copy(
108
                    format!("{path}/{output_filename}.generated-parse.serialised.json"),
109
                    format!("{path}/{output_filename}.expected-parse.serialised.json"),
110
                )?;
111
                std::fs::copy(
112
                    format!("{path}/{output_filename}.generated-essence.essence"),
113
                    format!("{path}/{output_filename}.expected-essence.essence"),
114
                )?;
115
            }
116

            
117
            // Ensures ACCEPT=true has been run at least once
118
            if !accept
119
                && !Path::new(&format!(
120
                    "{path}/{output_filename}.expected-parse.serialised.json"
121
                ))
122
                .exists()
123
            {
124
                return Err(Box::new(std::io::Error::new(
125
                    std::io::ErrorKind::NotFound,
126
                    format!("Expected output file not found: Run with ACCEPT=true"),
127
                )));
128
            }
129

            
130
            // Compare the expected and generated model
131
            let expected_model =
132
                read_model_json(&context, path, output_filename, "expected", "parse")?;
133
            let generated_model =
134
                read_model_json(&context, path, output_filename, "generated", "parse")?;
135
            assert_eq!(generated_model, expected_model);
136

            
137
            // Compares essence files
138
            let expected_essence = fs::read_to_string(&format!(
139
                "{path}/{output_filename}.expected-essence.essence"
140
            ))?;
141
            let generated_essence = fs::read_to_string(&format!(
142
                "{path}/{output_filename}.generated-essence.essence"
143
            ))?;
144
            assert_eq!(expected_essence, generated_essence);
145

            
146
            // Compares roundtrip
147
            let new_model = parse(
148
                &format!("{path}/{output_filename}.generated-essence.essence"),
149
                context.clone(),
150
            )?;
151
            save_essence(&new_model, path, output_filename, "generated2")?;
152
            let new_generated_essence = fs::read_to_string(&format!(
153
                "{path}/{output_filename}.generated2-essence.essence"
154
            ))?;
155
            assert_eq!(generated_essence, new_generated_essence);
156
        }
157

            
158
        Err(parse_error) => {
159
            save_parse_error(&parse_error, path, output_filename, "generated")?;
160

            
161
            // When ACCEPT = true, copy over generated to expected
162
            if accept {
163
                std::fs::copy(
164
                    format!("{path}/{output_filename}.generated-error.txt"),
165
                    format!("{path}/{output_filename}.expected-error.txt"),
166
                )?;
167
            }
168

            
169
            // Ensures ACCEPT=true has been run at least once
170
            if !accept
171
                && !Path::new(&format!("{path}/{output_filename}.expected-error.txt")).exists()
172
            {
173
                return Err(Box::new(std::io::Error::new(
174
                    std::io::ErrorKind::NotFound,
175
                    format!("Expected output file not found: Run with ACCEPT=true"),
176
                )));
177
            }
178

            
179
            let expected_error =
180
                fs::read_to_string(&format!("{path}/{output_filename}.expected-error.txt"))?;
181
            let generated_error =
182
                fs::read_to_string(&format!("{path}/{output_filename}.generated-error.txt"))?;
183
            assert_eq!(expected_error, generated_error);
184
        }
185
    }
186

            
187
    Ok(())
188
}
189

            
190
/* Saves a model as an Essence file */
191
fn save_essence(
192
    model: &Model,
193
    path: &str,
194
    test_name: &str,
195
    model_type: &str,
196
) -> Result<(), std::io::Error> {
197
    let filename = format!("{path}/{test_name}.{model_type}-essence.essence");
198
    let mut file = fs::File::create(&filename)?;
199
    write!(file, "{}", model)?;
200
    Ok(())
201
}
202

            
203
/* Saves a error message as a text file */
204
fn save_parse_error(
205
    error: &EssenceParseError,
206
    path: &str,
207
    test_name: &str,
208
    model_type: &str,
209
) -> Result<(), std::io::Error> {
210
    let filename = format!("{path}/{test_name}.{model_type}-error.txt");
211
    let mut file = fs::File::create(&filename)?;
212
    write!(file, "{}", error)?;
213
    Ok(())
214
}
215

            
216
include!(concat!(env!("OUT_DIR"), "/gen_tests_roundtrip.rs"));