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