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
2
    fn default() -> Self {
27
2
        Self {
28
2
            parsers: vec![format!("legacy"), format!("native")],
29
2
        }
30
2
    }
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
30
fn roundtrip_test(path: &str, filename: &str, extension: &str) -> Result<(), Box<dyn Error>> {
36
    // Reads in a config.toml in the test directory
37
30
    let file_config: TestConfig =
38
30
        if let Ok(config_contents) = fs::read_to_string(format!("{path}/config.toml")) {
39
28
            toml::from_str(&config_contents).unwrap()
40
        } else {
41
2
            Default::default()
42
        };
43
    // Runs native parser
44
30
    if file_config.parsers.contains(&format!("native")) {
45
3
        let new_filename = filename.to_owned() + "-native";
46
3
        roundtrip_test_inner(
47
3
            path,
48
3
            &filename,
49
3
            &new_filename,
50
3
            extension,
51
3
            parse_essence_file_native,
52
        )?;
53
27
    }
54
    // Runs legacy Conjure parser
55
30
    if file_config.parsers.contains(&format!("legacy")) {
56
29
        let new_filename = filename.to_owned() + "-legacy";
57
29
        roundtrip_test_inner(
58
29
            path,
59
29
            &filename,
60
29
            &new_filename,
61
29
            extension,
62
29
            parse_essence_file,
63
        )?;
64
1
    }
65
30
    Ok(())
66
30
}
67

            
68
// Runs the test for either parser
69
32
fn roundtrip_test_inner(
70
32
    path: &str,
71
32
    input_filename: &str,
72
32
    output_filename: &str,
73
32
    extension: &str,
74
32
    parse: fn(&str, Arc<RwLock<Context<'static>>>) -> Result<Model, EssenceParseError>,
75
32
) -> 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
32
    let accept = env::var("ACCEPT").unwrap_or("false".to_string()) == "true";
95

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

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

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

            
117
            // Ensures ACCEPT=true has been run at least once
118
30
            if !accept
119
30
                && !Path::new(&format!(
120
30
                    "{path}/agnostic-{output_filename}.expected-parse.serialised.json"
121
30
                ))
122
30
                .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
30
            }
129

            
130
            // Compare the expected and generated model
131
30
            let expected_model =
132
30
                read_model_json(&context, path, output_filename, "expected", "parse", None)?;
133

            
134
30
            let generated_model =
135
30
                read_model_json(&context, path, output_filename, "generated", "parse", None)?;
136
30
            assert_eq!(generated_model, expected_model);
137

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

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

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

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

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

            
183
2
            let expected_error = fs::read_to_string(&format!(
184
2
                "{path}/agnostic-{output_filename}.expected-error.txt"
185
2
            ))?;
186
2
            let generated_error = fs::read_to_string(&format!(
187
2
                "{path}/agnostic-{output_filename}.generated-error.txt"
188
2
            ))?;
189
2
            assert_eq!(expected_error, generated_error);
190
        }
191
    }
192

            
193
32
    Ok(())
194
32
}
195

            
196
/* Saves a model as an Essence file */
197
60
fn save_essence(
198
60
    model: &Model,
199
60
    path: &str,
200
60
    test_name: &str,
201
60
    model_type: &str,
202
60
) -> Result<(), std::io::Error> {
203
60
    let filename = format!("{path}/agnostic-{test_name}.{model_type}-essence.essence");
204
60
    let mut file = fs::File::create(&filename)?;
205
60
    write!(file, "{}", model)?;
206
60
    Ok(())
207
60
}
208

            
209
/* Saves a error message as a text file */
210
2
fn save_parse_error(
211
2
    error: &EssenceParseError,
212
2
    path: &str,
213
2
    test_name: &str,
214
2
    model_type: &str,
215
2
) -> Result<(), std::io::Error> {
216
2
    let filename = format!("{path}/agnostic-{test_name}.{model_type}-error.txt");
217
2
    let mut file = fs::File::create(&filename)?;
218
2
    write!(file, "{}", error)?;
219
2
    Ok(())
220
2
}
221

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