Skip to main content

bind/
test.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::compiler::{SymbolTable, SymbolicInstructionInfo, compiler};
6use crate::debugger::device_specification::DeviceSpecification;
7use crate::debugger::offline_debugger::{self, debug_from_device_specification};
8use crate::errors::UserError;
9use crate::parser;
10use serde::Deserialize;
11use std::collections::HashMap;
12use std::fmt;
13use thiserror::Error;
14use valico::json_schema;
15
16const BIND_SCHEMA: &str = include_str!("../tests_schema.json");
17const COMPOSITE_SCHEMA: &str = include_str!("../composite_tests_schema.json");
18
19#[derive(Deserialize, Debug, PartialEq)]
20#[serde(rename_all = "lowercase")]
21enum ExpectedResult {
22    Match,
23    Abort,
24}
25
26#[derive(Deserialize, Debug, PartialEq)]
27pub struct BindSpec {
28    name: String,
29    expected: ExpectedResult,
30    device: HashMap<String, String>,
31}
32
33#[derive(Deserialize, Debug, PartialEq)]
34pub struct CompositeParentSpec {
35    #[serde(alias = "node")]
36    pub parent: String,
37    pub tests: Vec<BindSpec>,
38}
39
40#[derive(Deserialize, Debug)]
41enum TestSpec {
42    Bind(Vec<BindSpec>),
43    CompositeBind(Vec<CompositeParentSpec>),
44}
45
46struct TestSuite {
47    specs: TestSpec,
48}
49
50#[derive(Debug, Error, Clone, PartialEq)]
51pub enum TestError {
52    BindParserError(parser::common::BindParserError),
53    DeviceSpecParserError(parser::common::BindParserError),
54    DebuggerError(offline_debugger::DebuggerError),
55    CompilerError(compiler::CompilerError),
56    InvalidSchema,
57    JsonParserError(String),
58    CompositeParentMissing(String),
59    // The JSON validator unfortunately doesn't produce useful error messages.
60    InvalidJsonError,
61}
62
63pub fn run(rules: &str, libraries: &[String], tests: &str) -> Result<bool, TestError> {
64    TestSuite::try_from(tests).and_then(|t| t.run(rules, libraries))
65}
66
67impl TestSuite {
68    fn run(&self, rules: &str, libraries: &[String]) -> Result<bool, TestError> {
69        match &self.specs {
70            TestSpec::Bind(test_specs) => {
71                let bind_rules =
72                    compiler::compile_bind(rules, libraries, false, false, false, false)
73                        .map_err(TestError::CompilerError)?;
74                run_bind_test_specs(test_specs, &bind_rules.symbol_table, &bind_rules.instructions)
75            }
76            TestSpec::CompositeBind(test_specs) => {
77                run_composite_bind_test_specs(test_specs, rules, libraries)
78            }
79        }
80    }
81}
82
83impl TryFrom<&str> for TestSuite {
84    type Error = TestError;
85
86    fn try_from(input: &str) -> Result<Self, Self::Error> {
87        // Try the non-composite bind test spec.
88        let bind_specs = parse_bind_spec(input);
89        if bind_specs.is_ok() {
90            return bind_specs;
91        }
92
93        // Try the composite bind test spec.
94        let composite_specs: Vec<CompositeParentSpec> =
95            serde_json::from_value(validate_spec(input, &COMPOSITE_SCHEMA)?)
96                .map_err(|e| TestError::JsonParserError(format!("{}", e)))?;
97        Ok(TestSuite { specs: TestSpec::CompositeBind(composite_specs) })
98    }
99}
100
101fn parse_bind_spec(input: &str) -> Result<TestSuite, TestError> {
102    let bind_specs = serde_json::from_value::<Vec<BindSpec>>(validate_spec(input, &BIND_SCHEMA)?)
103        .map_err(|e| TestError::JsonParserError(format!("{}", e)))?;
104    Ok(TestSuite { specs: TestSpec::Bind(bind_specs) })
105}
106
107fn validate_spec(input: &str, schema: &str) -> Result<serde_json::Value, TestError> {
108    let schema =
109        serde_json::from_str(&schema).map_err(|e| TestError::JsonParserError(format!("{}", e)))?;
110    let mut scope = json_schema::Scope::new();
111    let compiled_schema =
112        scope.compile_and_return(schema, false).map_err(|_| TestError::InvalidSchema)?;
113
114    let spec =
115        serde_json::from_str(input).map_err(|e| TestError::JsonParserError(format!("{}", e)))?;
116
117    let res = compiled_schema.validate(&spec);
118    if !res.is_strictly_valid() {
119        return Err(TestError::InvalidJsonError);
120    }
121
122    Ok(spec)
123}
124
125fn run_bind_test_specs<'a>(
126    specs: &Vec<BindSpec>,
127    symbol_table: &SymbolTable,
128    instructions: &Vec<SymbolicInstructionInfo<'a>>,
129) -> Result<bool, TestError> {
130    println!("[----------]");
131    for test in specs {
132        let mut device_specification = DeviceSpecification::new();
133        println!("[ RUN      ] {}", test.name);
134        for (key, value) in &test.device {
135            let result = device_specification
136                .add_property(&key, &value)
137                .map_err(TestError::DeviceSpecParserError);
138            if let Err(e) = result {
139                println!("Failed to add specification {key}:{value}");
140                return Err(e);
141            }
142        }
143
144        let result =
145            debug_from_device_specification(symbol_table, instructions, device_specification)
146                .map_err(TestError::DebuggerError)?;
147        match (&test.expected, result) {
148            (ExpectedResult::Match, false) | (ExpectedResult::Abort, true) => {
149                println!("[  FAILED  ] {}", test.name);
150                println!("[----------]");
151                return Ok(false);
152            }
153            _ => (),
154        }
155        println!("[       OK ] {}", test.name);
156    }
157    println!("[  PASSED  ]");
158    println!("[----------]");
159    Ok(true)
160}
161
162fn run_composite_bind_test_specs(
163    specs: &Vec<CompositeParentSpec>,
164    rules: &str,
165    libraries: &[String],
166) -> Result<bool, TestError> {
167    let composite_bind = compiler::compile_bind_composite(rules, libraries, false, false, false)
168        .map_err(TestError::CompilerError)?;
169
170    // Map composite bind rules by parent name.
171    let mut parent_map: HashMap<String, Vec<SymbolicInstructionInfo>> = HashMap::new();
172    parent_map
173        .insert(composite_bind.primary_parent.name, composite_bind.primary_parent.instructions);
174    for parent in composite_bind.additional_parents {
175        parent_map.insert(parent.name, parent.instructions);
176    }
177
178    for parent in composite_bind.optional_parents {
179        parent_map.insert(parent.name, parent.instructions);
180    }
181
182    println!("[==========]");
183    for parent_spec in specs {
184        println!("[ RUN      ] Test for composite parent {}", parent_spec.parent);
185        if !parent_map.contains_key(&parent_spec.parent) {
186            println!("[  FAILED  ] {}", parent_spec.parent);
187            println!("[==========]");
188            return Err(TestError::CompositeParentMissing(parent_spec.parent.clone()));
189        }
190
191        if !run_bind_test_specs(
192            &parent_spec.tests,
193            &composite_bind.symbol_table,
194            parent_map.get(&parent_spec.parent).unwrap(),
195        )? {
196            println!("[  FAILED  ] {}", parent_spec.parent);
197            println!("[==========]");
198            return Ok(false);
199        }
200        println!("[       OK ] {}", parent_spec.parent);
201    }
202    println!("[  SUCCESS  ] ");
203    println!("[==========]");
204    Ok(true)
205}
206
207impl fmt::Display for TestError {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        write!(f, "{}", UserError::from(self.clone()))
210    }
211}
212
213#[cfg(test)]
214mod test {
215    use super::*;
216    use assert_matches::assert_matches;
217
218    #[test]
219    fn parse_one_test() {
220        let TestSuite { specs } = TestSuite::try_from(
221            r#"
222[
223    {
224        "name": "A test",
225        "expected": "match",
226        "device": {
227            "key": "value"
228        }
229    }
230]"#,
231        )
232        .unwrap();
233
234        assert_matches!(specs, TestSpec::Bind(_));
235
236        if let TestSpec::Bind(specs) = specs {
237            assert_eq!(specs.len(), 1);
238            assert_eq!(specs[0].name, "A test".to_string());
239            assert_eq!(specs[0].expected, ExpectedResult::Match);
240
241            let mut expected_device = HashMap::new();
242            expected_device.insert("key".to_string(), "value".to_string());
243            assert_eq!(specs[0].device, expected_device);
244        }
245    }
246
247    #[test]
248    fn parse_two_tests() {
249        let TestSuite { specs } = TestSuite::try_from(
250            r#"
251    [
252        {
253            "name": "A test",
254            "expected": "match",
255            "device": {
256                "key": "value"
257            }
258        },
259        {
260            "name": "A second test",
261            "expected": "abort",
262            "device": {
263                "key1": "value1",
264                "key2": "value2"
265            }
266        }
267    ]"#,
268        )
269        .unwrap();
270
271        assert_matches!(specs, TestSpec::Bind(_));
272        if let TestSpec::Bind(specs) = specs {
273            assert_eq!(specs.len(), 2);
274            assert_eq!(specs[0].name, "A test".to_string());
275            assert_eq!(specs[0].expected, ExpectedResult::Match);
276            assert_eq!(specs[1].name, "A second test".to_string());
277            assert_eq!(specs[1].expected, ExpectedResult::Abort);
278
279            let mut expected_device = HashMap::new();
280            expected_device.insert("key".to_string(), "value".to_string());
281            assert_eq!(specs[0].device, expected_device);
282
283            let mut expected_device2 = HashMap::new();
284            expected_device2.insert("key1".to_string(), "value1".to_string());
285            expected_device2.insert("key2".to_string(), "value2".to_string());
286            assert_eq!(specs[1].device, expected_device2);
287        }
288    }
289
290    #[test]
291    fn parse_one_composite_parent() {
292        let TestSuite { specs } = TestSuite::try_from(
293            r#"
294            [
295                {
296                    "node": "honeycreeper",
297                    "tests": [
298                        {
299                            "name": "tanager",
300                            "expected": "match",
301                            "device": {
302                                "grassquit": "blue-black",
303                                "flowerpiercer": "moustached"
304                            }
305                        }
306                    ]
307                }
308            ]"#,
309        )
310        .unwrap();
311
312        let mut expected_device = HashMap::new();
313        expected_device.insert("grassquit".to_string(), "blue-black".to_string());
314        expected_device.insert("flowerpiercer".to_string(), "moustached".to_string());
315
316        let expected_specs = CompositeParentSpec {
317            parent: "honeycreeper".to_string(),
318            tests: vec![BindSpec {
319                name: "tanager".to_string(),
320                expected: ExpectedResult::Match,
321                device: expected_device,
322            }],
323        };
324
325        assert_matches!(specs, TestSpec::CompositeBind(_));
326        if let TestSpec::CompositeBind(parent_specs) = specs {
327            assert_eq!(1, parent_specs.len());
328            assert_eq!(expected_specs, parent_specs[0]);
329        }
330    }
331
332    #[test]
333    fn parse_multiple_composite_parent() {
334        let TestSuite { specs } = TestSuite::try_from(
335            r#"
336            [
337                {
338                    "node": "honeycreeper",
339                    "tests": [
340                        {
341                            "name": "tanager",
342                            "expected": "match",
343                            "device": {
344                                "grassquit": "blue-black",
345                                "flowerpiercer": "moustached"
346                            }
347                        }
348                    ]
349                },
350                {
351                    "node": "ground-roller",
352                    "tests": [
353                        {
354                            "name": "kingfisher",
355                            "expected": "match",
356                            "device": {
357                                "bee-eater": "little"
358                            }
359                        }
360                    ]
361                }
362            ]"#,
363        )
364        .unwrap();
365
366        let mut expected_device_1 = HashMap::new();
367        expected_device_1.insert("grassquit".to_string(), "blue-black".to_string());
368        expected_device_1.insert("flowerpiercer".to_string(), "moustached".to_string());
369
370        let mut expected_device_2 = HashMap::new();
371        expected_device_2.insert("bee-eater".to_string(), "little".to_string());
372
373        let expected_specs = [
374            CompositeParentSpec {
375                parent: "honeycreeper".to_string(),
376                tests: vec![BindSpec {
377                    name: "tanager".to_string(),
378                    expected: ExpectedResult::Match,
379                    device: expected_device_1,
380                }],
381            },
382            CompositeParentSpec {
383                parent: "ground-roller".to_string(),
384                tests: vec![BindSpec {
385                    name: "kingfisher".to_string(),
386                    expected: ExpectedResult::Match,
387                    device: expected_device_2,
388                }],
389            },
390        ];
391
392        assert_matches!(specs, TestSpec::CompositeBind(_));
393        if let TestSpec::CompositeBind(parent_specs) = specs {
394            assert_eq!(expected_specs.len(), parent_specs.len());
395            for i in 0..expected_specs.len() {
396                assert_eq!(expected_specs[i], parent_specs[i]);
397            }
398        }
399    }
400
401    #[test]
402    fn parse_json_failure() {
403        match TestSuite::try_from("this can't be parsed") {
404            Err(TestError::JsonParserError(_)) => (),
405            _ => panic!("Expected a JsonParserError"),
406        }
407    }
408
409    #[test]
410    fn parse_invalid_json() {
411        match TestSuite::try_from(r#"{ "valid json": "invalid to spec" }"#) {
412            Err(TestError::InvalidJsonError) => (),
413            _ => panic!("Expected a InvalidJsonError"),
414        };
415    }
416
417    #[test]
418    fn test_missing_parent() {
419        let test_suite = TestSuite::try_from(
420            r#"
421            [
422                {
423                    "node": "tyrant",
424                    "tests": [
425                        {
426                            "name": "tyrannulet",
427                            "expected": "match",
428                            "device": {
429                                "water-tyrant": "pied"
430                            }
431                        }
432                    ]
433                }
434            ]"#,
435        )
436        .unwrap();
437
438        let composite_bind_rules = "composite flycatcher;
439            primary parent \"pewee\" {
440                fuchsia.BIND_PROTOCOL == 1;
441            }
442            parent \"phoebe\" {
443                fuchsia.BIND_PROTOCOL == 2;
444            }";
445
446        assert_eq!(
447            Err(TestError::CompositeParentMissing("tyrant".to_string())),
448            test_suite.run(composite_bind_rules, &[])
449        );
450    }
451}