1use 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 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 let bind_specs = parse_bind_spec(input);
88 if bind_specs.is_ok() {
89 return bind_specs;
90 }
91
92 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 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}