diff --git a/scripts/yaml-test-eval b/scripts/yaml-test-eval index 5f6f943..7bc185c 100755 --- a/scripts/yaml-test-eval +++ b/scripts/yaml-test-eval @@ -5,4 +5,4 @@ set -e yaml=$(realpath -e $1) -RUST_BACKTRACE=1 cargo test interpreter::one_yaml -- --include-ignored --nocapture "$yaml" $2 +RUST_BACKTRACE=1 cargo test interpreter::one_yaml -- --include-ignored --nocapture "$yaml" diff --git a/src/builtins/bitwise.rs b/src/builtins/bitwise.rs index c866e40..5597fce 100644 --- a/src/builtins/bitwise.rs +++ b/src/builtins/bitwise.rs @@ -52,6 +52,11 @@ fn lsh(span: &Span, params: &[Expr], args: &[Value]) -> Result { // TODO: precision let v1 = v1 as i64; let v2 = v2 as i64; + + if v2 <= 0 { + return Ok(Value::Undefined); + } + Ok(Value::from_float((v1 << v2) as Float)) } @@ -101,6 +106,11 @@ fn rsh(span: &Span, params: &[Expr], args: &[Value]) -> Result { // TODO: precision let v1 = v1 as i64; let v2 = v2 as i64; + + if v2 < 0 { + return Ok(Value::Undefined); + } + Ok(Value::from_float((v1 >> v2) as Float)) } diff --git a/src/builtins/strings.rs b/src/builtins/strings.rs index 0392768..add681a 100644 --- a/src/builtins/strings.rs +++ b/src/builtins/strings.rs @@ -20,7 +20,8 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { m.insert("endswith", (endswith, 2)); m.insert("format_int", (format_int, 2)); m.insert("indexof", (indexof, 2)); - m.insert("indexof_n", (indexof_n, 2)); + // TODO: implement this correctly. + //m.insert("indexof_n", (indexof_n, 2)); m.insert("lower", (lower, 1)); m.insert("replace", (replace, 3)); m.insert("split", (split, 2)); @@ -98,6 +99,7 @@ fn indexof(span: &Span, params: &[Expr], args: &[Value]) -> Result { } as Float)) } +#[allow(dead_code)] fn indexof_n(span: &Span, params: &[Expr], args: &[Value]) -> Result { let name = "indexof_n"; ensure_args_count(span, name, params, args, 2)?; diff --git a/src/parser.rs b/src/parser.rs index 3cbd2ac..6534db0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -333,7 +333,7 @@ impl<'source> Parser<'source> { } // It could be a set, object or object comprehension. - // In all the cases, the first expressoin must parse successfully. + // In all the cases, the first expression must parse successfully. if *self.tok.1.text() == "}" { self.next_token()?; span.end = self.end; diff --git a/tests/interpreter/mod.rs b/tests/interpreter/mod.rs index dafe041..f103a78 100644 --- a/tests/interpreter/mod.rs +++ b/tests/interpreter/mod.rs @@ -1,18 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#![cfg(test)] - use std::env; -use std::path::Path; use anyhow::{bail, Result}; use regorus::*; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; use test_generator::test_resources; -use walkdir::WalkDir; - -mod cases; // Process test value specified in json/yaml to interpret special encodings. pub fn process_value(v: &Value) -> Result { @@ -191,91 +185,6 @@ fn push_query_results(query_results: QueryResults, results: &mut Vec) { } } -pub fn eval_file_first_rule( - regos: &[String], - data_opt: Option, - input_opt: Option, - query: &str, - enable_tracing: bool, -) -> Result> { - let mut results = vec![]; - let mut files = vec![]; - let mut sources = vec![]; - let mut modules = vec![]; - let mut modules_ref = vec![]; - - for (idx, _) in regos.iter().enumerate() { - files.push(format!("rego_{idx}")); - } - - for (idx, file) in files.iter().enumerate() { - let contents = regos[idx].as_str(); - sources.push(Source::new(file.to_string(), contents.to_string())); - } - - for source in &sources { - let mut parser = Parser::new(source)?; - modules.push(parser.parse()?); - } - - for m in &modules { - modules_ref.push(m); - } - - let query_source = regorus::Source::new("".to_string(), query.to_string()); - let query_span = regorus::Span { - source: query_source.clone(), - line: 1, - col: 1, - start: 0, - end: query.len() as u16, - }; - let mut parser = regorus::Parser::new(&query_source)?; - let query_node = parser.parse_query(query_span, "")?; - let query_schedule = - regorus::Analyzer::new().analyze_query_snippet(&modules_ref, &query_node)?; - let analyzer = Analyzer::new(); - let schedule = analyzer.analyze(&modules_ref)?; - - let mut interpreter = interpreter::Interpreter::new(&modules_ref)?; - if let Some(input) = input_opt { - // if inputs are defined then first the evaluation if prepared - interpreter.prepare_for_eval(Some(schedule), &data_opt)?; - - // then all modules are evaluated for each input - let mut inputs = vec![]; - match input { - ValueOrVec::Single(single_input) => inputs.push(single_input), - ValueOrVec::Many(mut many_input) => inputs.append(&mut many_input), - } - - for input in inputs { - if let Some(module) = &modules.get(0) { - if let Some(rule) = &module.policy.get(0) { - interpreter.eval_rule_with_input(module, rule, &Some(input), enable_tracing)?; - } - } - - // Now eval the query. - push_query_results( - interpreter.eval_user_query(&query_node, &query_schedule, enable_tracing)?, - &mut results, - ); - } - } else { - // it no input is defined then one evaluation of all modules is performed - interpreter.eval(&data_opt, &None, enable_tracing, Some(schedule))?; - - // Now eval the query - push_query_results( - interpreter.eval_user_query(&query_node, &query_schedule, enable_tracing)?, - &mut results, - ); - } - - Ok(results) -} - pub fn eval_file( regos: &[String], data_opt: Option, @@ -415,7 +324,7 @@ struct YamlTest { cases: Vec, } -fn yaml_test_impl(file: &str, is_opa_test: bool) -> Result<()> { +fn yaml_test_impl(file: &str) -> Result<()> { let yaml_str = std::fs::read_to_string(file)?; let test: YamlTest = serde_yaml::from_str(&yaml_str)?; @@ -430,7 +339,6 @@ fn yaml_test_impl(file: &str, is_opa_test: bool) -> Result<()> { match (&case.want_result, &case.error) { (Some(_), None) | (None, Some(_)) => (), - _ if is_opa_test => (), _ => panic!("either want_result or error must be specified in test case."), } @@ -453,23 +361,10 @@ fn yaml_test_impl(file: &str, is_opa_test: bool) -> Result<()> { } } - if is_opa_test { - // Convert value to json compatible representation. - let results = - Value::from_json_str(serde_json::to_string(&results)?.as_str())?; - match_values(&results, &expected_results[0])?; - } else { - check_output(&results, &expected_results)?; - } + check_output(&results, &expected_results)?; } _ => bail!("eval succeeded and did not produce any errors"), }, - Err(actual) if is_opa_test => { - if case.want_error.is_none() && case.want_error_code.is_none() { - return Err(actual); - } - // opa test expects execution to fail and it did. - } Err(actual) => match &case.error { Some(expected) => { let actual = actual.to_string(); @@ -492,8 +387,8 @@ fn yaml_test_impl(file: &str, is_opa_test: bool) -> Result<()> { Ok(()) } -fn yaml_test(file: &str, is_opa_test: bool) -> Result<()> { - match yaml_test_impl(file, is_opa_test) { +fn yaml_test(file: &str) -> Result<()> { + match yaml_test_impl(file) { Ok(_) => Ok(()), Err(e) => { // If Err is returned, it doesn't always get printed by cargo test. @@ -505,20 +400,17 @@ fn yaml_test(file: &str, is_opa_test: bool) -> Result<()> { #[test] fn yaml_test_basic() -> Result<()> { - yaml_test("tests/interpreter/cases/basic_001.yaml", false) + yaml_test("tests/interpreter/cases/basic_001.yaml") } #[test] #[ignore = "intended for use by scripts/yaml-test-eval"] fn one_yaml() -> Result<()> { let mut file = String::default(); - let mut is_opa_test = false; for a in env::args() { if a.ends_with(".yaml") { file = a; - } else if a == "opa-test" { - is_opa_test = true; } } @@ -526,47 +418,10 @@ fn one_yaml() -> Result<()> { bail!("missing "); } - yaml_test(file.as_str(), is_opa_test) + yaml_test(file.as_str()) } #[test_resources("tests/interpreter/**/*.yaml")] fn run(path: &str) { - yaml_test(path, false).unwrap() -} - -#[test] -#[ignore = "intended for running opa test suite"] -fn run_opa_tests() -> Result<()> { - let mut failures = vec![]; - for a in env::args() { - if !Path::new(&a).is_dir() { - continue; - } - - for entry in WalkDir::new(a) - .sort_by_file_name() - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path().to_string_lossy().to_string(); - if !Path::new(&path).is_file() || !path.ends_with(".yaml") { - continue; - } - let yaml = path; - match yaml_test_impl(yaml.as_str(), true) { - Ok(_) => (), - Err(e) => { - failures.push((yaml, e)); - } - } - } - } - - if !failures.is_empty() { - for (f, e) in failures { - println!("{f} failed.\n{e}"); - } - panic!("failed"); - } - Ok(()) + yaml_test(path).unwrap() } diff --git a/tests/lexer/mod.rs b/tests/lexer/mod.rs index b7fa230..3c3b7ee 100644 --- a/tests/lexer/mod.rs +++ b/tests/lexer/mod.rs @@ -4,7 +4,6 @@ use anyhow::{bail, Result}; use regorus::*; use serde::{Deserialize, Serialize}; -use std::env; use test_generator::test_resources; fn get_tokens(source: &Source) -> Result> { @@ -155,24 +154,6 @@ fn yaml_test(file: &str) -> Result<()> { } } -#[test] -#[ignore = "intended for use by scripts/yaml-test-lex"] -fn one_yaml() -> Result<()> { - let mut file = String::default(); - for a in env::args() { - if a.ends_with(".yaml") { - file = a; - break; - } - } - - if file.is_empty() { - bail!("missing yaml test file"); - } - - yaml_test(file.as_str()) -} - #[test_resources("tests/lexer/**/*.yaml")] fn run(path: &str) { yaml_test(path).unwrap() diff --git a/tests/opa/mod.rs b/tests/opa/mod.rs new file mode 100644 index 0000000..a89747f --- /dev/null +++ b/tests/opa/mod.rs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use regorus::*; + +use std::collections::BTreeMap; +use std::path::Path; + +use anyhow::Result; +use walkdir::WalkDir; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct TestCase { + data: Option, + input: Option, + modules: Option>, + note: String, + query: String, + sort_bindings: Option, + want_result: Option, + skip: Option, + error: Option, + traces: Option, + want_error: Option, + want_error_code: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct YamlTest { + cases: Vec, +} + +fn eval_test_case(case: &TestCase) -> Result { + let mut engine = Engine::new(); + + if let Some(data) = &case.data { + engine.add_data(data.clone())?; + } + if let Some(input) = &case.input { + engine.set_input(input.clone()); + } + if let Some(modules) = &case.modules { + for (idx, rego) in modules.iter().enumerate() { + engine.add_policy(format!("rego{idx}.rego"), rego.clone())?; + } + } + let query_results = engine.eval_query(case.query.clone(), true)?; + + let mut values = vec![]; + for qr in query_results.result { + values.push(if !qr.bindings.is_empty_object() { + qr.bindings.clone() + } else if let Some(v) = qr.expressions.last() { + v["value"].clone() + } else { + Value::Undefined + }); + } + let result = Value::from_array(values); + // Make result json compatible. (E.g: avoid sets). + Value::from_json_str(&result.to_string()) +} + +#[test] +fn run_opa_tests() -> Result<()> { + let opa_tests_dir = match std::env::var("OPA_TESTS_DIR") { + Ok(v) => v, + _ => { + println!("OPA_TESTS_DIR environment vairable not defined."); + return Ok(()); + } + }; + dbg!(&opa_tests_dir); + let tests_path = Path::new(&opa_tests_dir); + let mut status = BTreeMap::::new(); + let mut n = 0; + for entry in WalkDir::new(&opa_tests_dir) + .sort_by_file_name() + .into_iter() + .filter_map(|e| e.ok()) + { + let path_str = entry.path().to_string_lossy().to_string(); + let path = Path::new(&path_str); + if !path.is_file() || !path_str.ends_with(".yaml") { + continue; + } + + let path_dir = path.strip_prefix(tests_path)?.parent().unwrap(); + + let path_dir_str = path_dir.to_string_lossy().to_string(); + let entry = status.entry(path_dir_str).or_insert((0, 0)); + + let yaml_str = std::fs::read_to_string(&path_str)?; + let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + + for case in &test.cases { + print!("{} ...", case.note); + match (eval_test_case(case), &case.want_result) { + (Ok(actual), Some(expected)) if &actual == expected => { + println!("passed"); + entry.0 += 1; + } + (Err(_), None) if case.want_error.is_some() => { + // Expected failure. + println!("passed"); + entry.0 += 1; + } + _ => { + let path = Path::new("target/opa").join(path_dir); + std::fs::create_dir_all(path.clone())?; + + if let Some(data) = &case.data { + std::fs::write( + path.join(format!("data{n}.json")), + data.to_json_str()?.as_bytes(), + )?; + }; + if let Some(input) = &case.input { + std::fs::write( + path.join(format!("input{n}.json")), + input.to_json_str()?.as_bytes(), + )?; + }; + + if let Some(modules) = &case.modules { + if modules.len() == 1 { + std::fs::write( + path.join(format!("rego{n}.rego")), + modules[0].as_bytes(), + )?; + } else { + for (i, m) in modules.iter().enumerate() { + std::fs::write( + path.join(format!("rego{n}_{i}.json")), + m.as_bytes(), + )?; + } + } + } + + println!("failed"); + entry.1 += 1; + n += 1; + continue; + } + }; + } + } + + println!("TESTSUITE STATUS"); + println!(" {:30} {:4} {:4}", "FOLDER", "PASS", "FAIL"); + for (dir, (pass, fail)) in status { + if fail == 0 { + println!("\x1b[32m {dir:40}: {pass:4} {fail:4}\x1b[0m"); + } else { + println!("\x1b[31m {dir:40}: {pass:4} {fail:4}\x1b[0m"); + } + } + + Ok(()) +} diff --git a/tests/parser/mod.rs b/tests/parser/mod.rs index 48954d3..5700356 100644 --- a/tests/parser/mod.rs +++ b/tests/parser/mod.rs @@ -4,7 +4,6 @@ use anyhow::{anyhow, bail, Result}; use regorus::*; use serde::{Deserialize, Serialize}; -use std::env; use test_generator::test_resources; macro_rules! my_assert_eq { @@ -700,24 +699,6 @@ fn yaml_test(file: &str) -> Result<()> { } } -#[test] -#[ignore = "intended for use by scripts/yaml-test-parse"] -fn one_yaml() -> Result<()> { - let mut file = String::default(); - for a in env::args() { - if a.ends_with(".yaml") { - file = a; - break; - } - } - - if file.is_empty() { - bail!("missing yaml test file"); - } - - yaml_test(file.as_str()) -} - #[test_resources("tests/parser/**/*.yaml")] fn run(path: &str) { yaml_test(path).unwrap() diff --git a/tests/tests.rs b/tests/tests.rs index c735c9a..599a121 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -3,6 +3,7 @@ mod interpreter; mod lexer; +mod opa; mod parser; mod scheduler; mod value;