thinlinelib/
config_parser.rs

1use failure::{err_msg, Fallible};
2use run_script::ScriptOptions;
3use std::{env, fs::read_to_string};
4use yaml_rust::{Yaml, YamlLoader};
5
6////////////////////////////////////////////////////////////////////////////////
7
8trait Conversion {
9    /// Consumes an Option<Vec<&'a str>> and returns it's elements as String.
10    fn to_string_vec(self) -> Vec<String>;
11}
12
13impl<'a> Conversion for Option<Vec<&'a str>> {
14    fn to_string_vec(self) -> Vec<String> {
15        if let Some(vec) = self {
16            vec.iter().map(|f| String::from(*f)).collect()
17        } else {
18            vec![]
19        }
20    }
21}
22
23////////////////////////////////////////////////////////////////////////////////
24
25trait ValueParser {
26    /// Reads a bool at the given config path.
27    fn get_bool(&self, keys: &[&str], default: bool) -> bool;
28
29    /// Reads a string at the given config path.
30    fn get_str(&self, keys: &[&str]) -> Option<&str>;
31
32    /// Reads a string vector at the given config path.
33    fn get_str_vec(&self, keys: &[&str]) -> Option<Vec<&str>>;
34}
35
36impl ValueParser for Yaml {
37    fn get_bool(&self, keys: &[&str], default: bool) -> bool {
38        let mut yml_obj = self;
39
40        for key in keys {
41            if yml_obj[*key].is_badvalue() {
42                return default;
43            }
44
45            yml_obj = &yml_obj[*key];
46            if let Some(yml_bool) = yml_obj.as_bool() {
47                return yml_bool;
48            }
49
50            if yml_obj[0].is_badvalue() {
51                continue;
52            }
53        }
54
55        default
56    }
57
58    fn get_str(&self, keys: &[&str]) -> Option<&str> {
59        let mut yml_obj = self;
60
61        for key in keys {
62            if yml_obj[*key].is_badvalue() {
63                return None;
64            }
65
66            yml_obj = &yml_obj[*key];
67            if let Some(yml_str) = yml_obj.as_str() {
68                return Some(yml_str);
69            }
70
71            if yml_obj[0].is_badvalue() {
72                continue;
73            }
74
75            if let Some(yml_str) = yml_obj[0].as_str() {
76                return Some(yml_str);
77            }
78        }
79
80        None
81    }
82
83    fn get_str_vec(&self, keys: &[&str]) -> Option<Vec<&str>> {
84        let mut yml_obj = self;
85        let mut yml_vec: Vec<&str> = Vec::new();
86
87        for key in keys {
88            if yml_obj[*key].is_badvalue() {
89                return None;
90            }
91
92            yml_obj = &yml_obj[*key];
93            let mut i = 0;
94            while !yml_obj[i].is_badvalue() {
95                if let Some(yml_str) = yml_obj[i].as_str() {
96                    yml_vec.push(yml_str);
97                }
98                i += 1;
99            }
100        }
101
102        if !yml_vec.is_empty() {
103            return Some(yml_vec);
104        }
105
106        None
107    }
108}
109
110////////////////////////////////////////////////////////////////////////////////
111
112#[derive(Default, Debug)]
113pub struct BuildScript {
114    /// Indicator whether the output is logged within thinline output or not.
115    pub log: bool,
116
117    /// Windows build steps.
118    pub windows: Vec<String>,
119
120    /// Linux build steps.
121    pub linux: Vec<String>,
122}
123
124impl BuildScript {
125    /// Executes the build script depending on the target OS.
126    ///
127    /// On Windows the script is executed with `cmd`, on linux with `sh`.
128    /// All build steps are concatenated with ` && `.
129    /// Given build steps are executed at the target project directory,
130    /// not at thinline directory. The yaml param `log` at `build_script`
131    /// section indicates whether the build outpus is print within the
132    /// thinline output (true) or the child process (false).
133    pub fn run(&self, dir: &str) -> Fallible<()> {
134        info!("Building target");
135
136        // Save current working dir
137        let current_working_dir = env::current_dir()?;
138
139        // Change to project dir
140        env::set_current_dir(&dir)?;
141
142        // Build script options
143        let mut options = ScriptOptions::new();
144
145        // Set the runner.
146        options.runner = if cfg!(target_os = "windows") {
147            Some(String::from("cmd"))
148        } else {
149            Some(String::from("sh"))
150        };
151
152        // Print it to the parent process output.
153        options.capture_output = !self.log;
154
155        // Format the commands depending on OS.
156        let cmd = if cfg!(target_os = "windows") {
157            format!(r#"{}"#, self.windows.join(" && "))
158        } else {
159            format!(r#"{}"#, self.linux.join(" && "))
160        };
161
162        // Run the script
163        let (code, _, _) = run_script::run(cmd.as_str(), &vec![], &options)?;
164
165        // Change back to thinline dir
166        env::set_current_dir(&current_working_dir)?;
167
168        // Check the return code
169        if code > 0 {
170            return Err(err_msg(format!(
171                "Executing build steps returned error code {}.",
172                code
173            )));
174        }
175
176        Ok(())
177    }
178}
179
180////////////////////////////////////////////////////////////////////////////////
181
182#[derive(Default, Debug)]
183/// The parsed project parameters.
184pub struct ProjectParameters {
185    /// The language of the source project (e.g. c, cpp or python)
186    pub language: String,
187
188    /// Test environment which should be used (e.g. google test)
189    pub test_env: String,
190
191    /// The build steps which should be executed when the build-option is set.
192    pub build_script: BuildScript,
193
194    /// Paths to libraries which should be linked.
195    pub lib_paths: Vec<String>,
196
197    /// The source directories to extract the test data.
198    pub source_dirs: Vec<String>,
199
200    /// The include directories necessary to build the tests.
201    pub include_dirs: Vec<String>,
202}
203
204impl ProjectParameters {
205    /// Parses the project parameters from the given yaml file.
206    pub fn parse(yml: &str) -> Fallible<ProjectParameters> {
207        if let Ok(yml_params) = YamlLoader::load_from_str(read_to_string(yml)?.as_str()) {
208            if let Some(yml_param) = yml_params.get(0) {
209                let mut params = ProjectParameters::default();
210
211                params.language =
212                    String::from(yml_param.get_str(&["language"]).ok_or_else(|| {
213                        err_msg("Unable to get parameters for mandatory 'language'.")
214                    })?);
215                params.test_env =
216                    String::from(yml_param.get_str(&["test_env"]).ok_or_else(|| {
217                        err_msg("Unable to get parameters for mandatory 'test_env'.")
218                    })?);
219
220                params.source_dirs = yml_param.get_str_vec(&["analysis_dirs"]).to_string_vec();
221                params.include_dirs = yml_param.get_str_vec(&["include_dirs"]).to_string_vec();
222                params.build_script.log = yml_param.get_bool(&["build_script", "log"], true);
223                params.build_script.linux = yml_param
224                    .get_str_vec(&["build_script", "linux"])
225                    .to_string_vec();
226                params.build_script.windows = yml_param
227                    .get_str_vec(&["build_script", "windows"])
228                    .to_string_vec();
229                params.lib_paths = yml_param.get_str_vec(&["libs"]).to_string_vec();
230
231                return Ok(params);
232            }
233        }
234
235        Err(format_err!("Unable to parse project parameters."))
236    }
237}
238
239////////////////////////////////////////////////////////////////////////////////
240
241#[cfg(test)]
242mod value_parser {
243    use super::ValueParser;
244    use std::fs::read_to_string;
245    use std::path::Path;
246    use yaml_rust::YamlLoader;
247
248    #[test]
249    fn parse_yaml_config_bool_succeed() {
250        let yml_path = Path::new("tests")
251            .join("testdata")
252            .join("config")
253            .join("config1.yml");
254        let yml_params =
255            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
256        let yml_param = yml_params.get(0);
257
258        let build_log = yml_param.unwrap().get_bool(&["build_script", "log"], false);
259        assert_eq!(build_log, true);
260    }
261
262    #[test]
263    fn parse_yaml_config_bool_failed() {
264        let yml_path = Path::new("tests")
265            .join("testdata")
266            .join("config")
267            .join("config1.yml");
268        let yml_params =
269            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
270        let yml_param = yml_params.get(0);
271
272        let build_log = yml_param
273            .unwrap()
274            .get_bool(&["build_script", "none_existing"], true);
275        assert_eq!(build_log, true);
276    }
277
278    #[test]
279    fn parse_yaml_config_str_succeed() {
280        let yml_path = Path::new("tests")
281            .join("testdata")
282            .join("config")
283            .join("config1.yml");
284        let yml_params =
285            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
286        let yml_param = yml_params.get(0);
287
288        let test_env = yml_param.unwrap().get_str(&["test_env"]);
289        assert_eq!(test_env, Some("ctest"));
290
291        let language = yml_param.unwrap().get_str(&["language"]);
292        assert_eq!(language, Some("c"));
293    }
294
295    #[test]
296    fn parse_yaml_config_str_failed() {
297        let yml_path = Path::new("tests")
298            .join("testdata")
299            .join("config")
300            .join("config1.yml");
301        let yml_params =
302            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
303        let yml_param = yml_params.get(0);
304
305        {
306            let test_env = yml_param.unwrap().get_str(&["none_existing"]);
307            assert!(test_env.is_none());
308        }
309
310        {
311            let test_env = yml_param.unwrap().get_str(&[]);
312            assert!(test_env.is_none());
313        }
314
315        let yml_path = Path::new("tests")
316            .join("testdata")
317            .join("config")
318            .join("config4.yml");
319        let yml_params =
320            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
321        let yml_param = yml_params.get(0);
322
323        {
324            let test_env = yml_param.unwrap().get_str(&["test_env"]);
325            assert!(test_env.is_none());
326        }
327    }
328
329    #[test]
330    fn parse_yaml_config_str_vec_failed() {
331        let yml_path = Path::new("tests")
332            .join("testdata")
333            .join("config")
334            .join("config1.yml");
335        let yml_params =
336            YamlLoader::load_from_str(read_to_string(yml_path).unwrap().as_str()).unwrap();
337        let yml_param = yml_params.get(0);
338
339        {
340            let test_env = yml_param.unwrap().get_str_vec(&["include_dirs"]);
341            assert_eq!(test_env, Some(vec!["include", "src"]));
342        }
343
344        {
345            let test_env = yml_param.unwrap().get_str_vec(&["none_existing"]);
346            assert!(test_env.is_none());
347        }
348
349        {
350            let test_env = yml_param.unwrap().get_str_vec(&[]);
351            assert!(test_env.is_none());
352        }
353    }
354}