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