mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
* specific functions added to eval different rego components Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * allow multiple inputs and results Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * prepare_for_eval is necessary to be called Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * test with the suggested code examples and clean scopes Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * try to refactor first steps of evaluations Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * improve coverage and fix clean state internal evaluation Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * add getters and setters and fix clean function Signed-off-by: eric-therond <eric.therond.fr@gmail.com> * Tests are single input by default. Multi input specified via "many!" marker. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> --------- Signed-off-by: eric-therond <eric.therond.fr@gmail.com> Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> Co-authored-by: eric-therond <eric.therond.fr@gmail.com>
539 lines
16 KiB
Rust
539 lines
16 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#![cfg(test)]
|
|
|
|
use std::env;
|
|
|
|
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<Value> {
|
|
match v {
|
|
// Handle Undefined encoded as a string "#undefined"
|
|
Value::String(s) if s == "#undefined" => Ok(Value::Undefined),
|
|
|
|
// Handle set encoded as an object
|
|
// set! :
|
|
// - item1
|
|
// - item2
|
|
// ...
|
|
Value::Object(ref fields) if fields.len() == 1 && matches!(&v["set!"], Value::Array(_)) => {
|
|
let mut set_value = Value::new_set();
|
|
let set = set_value.as_set_mut()?;
|
|
for item in v["set!"].as_array()? {
|
|
set.insert(process_value(item)?);
|
|
}
|
|
Ok(set_value)
|
|
}
|
|
|
|
// Handle complex object specified explicitly:
|
|
// object! :
|
|
// - key: ...
|
|
// value: ...
|
|
Value::Object(fields) if fields.len() == 1 && matches!(&v["object!"], Value::Array(_)) => {
|
|
let mut object_value = Value::new_object();
|
|
let object = object_value.as_object_mut()?;
|
|
for item in v["object!"].as_array()? {
|
|
object.insert(process_value(&item["key"])?, process_value(&item["value"])?);
|
|
}
|
|
Ok(object_value)
|
|
}
|
|
|
|
// Recursively process arrays
|
|
Value::Array(items) => {
|
|
let mut array_value = Value::new_array();
|
|
let array = array_value.as_array_mut()?;
|
|
for item in items.iter() {
|
|
array.push(process_value(item)?);
|
|
}
|
|
Ok(array_value)
|
|
}
|
|
|
|
// Recursively process objects
|
|
Value::Object(fields) => {
|
|
let mut object_value = Value::new_object();
|
|
let object = object_value.as_object_mut()?;
|
|
for (key, value) in fields.iter() {
|
|
object.insert(process_value(key)?, process_value(value)?);
|
|
}
|
|
Ok(object_value)
|
|
}
|
|
|
|
Value::Set(_) => bail!("unexpected set in value read from json/yaml"),
|
|
|
|
// Simple variants
|
|
_ => Ok(v.clone()),
|
|
}
|
|
}
|
|
|
|
fn display_values(c: &Value, e: &Value) -> Result<String> {
|
|
Ok(format!(
|
|
"\nleft = {}\nright = {}\n",
|
|
serde_json::to_string_pretty(c)?,
|
|
serde_json::to_string_pretty(e)?
|
|
))
|
|
}
|
|
|
|
// Helper function to match computed and expecte values.
|
|
// On mismatch, prints the failing sub-value instead of the whole value.
|
|
fn match_values_impl(computed: &Value, expected: &Value) -> Result<()> {
|
|
match (&computed, &expected) {
|
|
(Value::Array(a1), Value::Array(a2)) => {
|
|
if a1.len() != a2.len() {
|
|
bail!(
|
|
"array length mismatch: {} != {}{}",
|
|
a1.len(),
|
|
a2.len(),
|
|
display_values(computed, expected)?
|
|
);
|
|
}
|
|
|
|
for (idx, v1) in a1.iter().enumerate() {
|
|
match_values_impl(v1, &a2[idx])?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
(Value::Set(s1), Value::Set(s2)) => {
|
|
if s1.len() != s2.len() {
|
|
bail!(
|
|
"set length mismatch: {} != {}{}",
|
|
s1.len(),
|
|
s2.len(),
|
|
display_values(computed, expected)?
|
|
);
|
|
}
|
|
|
|
let mut itr2 = s2.iter();
|
|
for v1 in s1.iter() {
|
|
match_values_impl(v1, itr2.next().unwrap())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
(Value::Object(o1), Value::Object(o2)) => {
|
|
if o1.len() != o2.len() {
|
|
bail!(
|
|
"object length mismatch: {} != {}{}",
|
|
o1.len(),
|
|
o2.len(),
|
|
display_values(computed, expected)?
|
|
);
|
|
}
|
|
|
|
let mut itr2 = o2.iter();
|
|
for (k1, v1) in o1.iter() {
|
|
let (k2, v2) = itr2.next().unwrap();
|
|
match_values_impl(k1, k2)?;
|
|
match_values_impl(v1, v2)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
(Value::Number(n1), Value::Number(n2)) if n1 == n2 => Ok(()),
|
|
(Value::String(s1), Value::String(s2)) if s1 == s2 => Ok(()),
|
|
(Value::Bool(b1), Value::Bool(b2)) if b1 == b2 => Ok(()),
|
|
(Value::Null, Value::Null) => Ok(()),
|
|
(Value::Undefined, Value::Undefined) => Ok(()),
|
|
|
|
_ => bail!("value mismatch: {}", display_values(computed, expected)?),
|
|
}
|
|
}
|
|
|
|
fn match_values(computed: &Value, expected: &Value) -> Result<()> {
|
|
match match_values_impl(computed, expected) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => bail!("\nmismatch in {}{}", display_values(computed, expected)?, e),
|
|
}
|
|
}
|
|
|
|
pub fn assert_match(computed_results: Vec<Value>, expected_results: Vec<Value>) {
|
|
if computed_results.len() != expected_results.len() {
|
|
panic!(
|
|
"the number of computed results ({}) and expected results ({}) is not equal",
|
|
computed_results.len(),
|
|
expected_results.len()
|
|
);
|
|
}
|
|
|
|
for (n, expected_result) in expected_results.into_iter().enumerate() {
|
|
let expected = match process_value(&expected_result) {
|
|
Ok(e) => e,
|
|
_ => panic!("unable to process value :\n {expected_result:?}"),
|
|
};
|
|
|
|
if let Some(computed_result) = computed_results.get(n) {
|
|
match match_values(computed_result, &expected) {
|
|
Ok(()) => (),
|
|
Err(e) => panic!("{}", e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn eval_file_first_rule(
|
|
regos: &[String],
|
|
data_opt: Option<Value>,
|
|
input_opt: Option<ValueOrVec>,
|
|
query: &str,
|
|
enable_tracing: bool,
|
|
) -> Result<Vec<Value>> {
|
|
let mut results = vec![];
|
|
let mut files = vec![];
|
|
let mut sources = vec![];
|
|
let mut modules = vec![];
|
|
let mut modules_ref = vec![];
|
|
|
|
// the query is parsed for later
|
|
let source = Source {
|
|
file: "<query.rego>",
|
|
contents: query,
|
|
lines: query.split('\n').collect(),
|
|
};
|
|
let mut parser = Parser::new(&source)?;
|
|
let expr = parser.parse_membership_expr()?;
|
|
|
|
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 {
|
|
file,
|
|
contents,
|
|
lines: contents.split('\n').collect(),
|
|
});
|
|
}
|
|
|
|
for source in &sources {
|
|
let mut parser = Parser::new(source)?;
|
|
modules.push(parser.parse()?);
|
|
}
|
|
|
|
for m in &modules {
|
|
modules_ref.push(m);
|
|
}
|
|
|
|
let analyzer = Analyzer::new();
|
|
let schedule = analyzer.analyze(&modules)?;
|
|
|
|
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.
|
|
results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?);
|
|
}
|
|
} 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.
|
|
results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
pub fn eval_file(
|
|
regos: &[String],
|
|
data_opt: Option<Value>,
|
|
input_opt: Option<ValueOrVec>,
|
|
query: &str,
|
|
enable_tracing: bool,
|
|
) -> Result<Vec<Value>> {
|
|
let mut results = vec![];
|
|
let mut files = vec![];
|
|
let mut sources = vec![];
|
|
let mut modules = vec![];
|
|
let mut modules_ref = vec![];
|
|
|
|
// the query is parsed for later
|
|
let source = Source {
|
|
file: "<query.rego>",
|
|
contents: query,
|
|
lines: query.split('\n').collect(),
|
|
};
|
|
let mut parser = Parser::new(&source)?;
|
|
let expr = parser.parse_membership_expr()?;
|
|
|
|
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 {
|
|
file,
|
|
contents,
|
|
lines: contents.split('\n').collect(),
|
|
});
|
|
}
|
|
|
|
for source in &sources {
|
|
let mut parser = Parser::new(source)?;
|
|
modules.push(parser.parse()?);
|
|
}
|
|
|
|
for m in &modules {
|
|
modules_ref.push(m);
|
|
}
|
|
|
|
let analyzer = Analyzer::new();
|
|
let schedule = analyzer.analyze(&modules)?;
|
|
|
|
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 {
|
|
interpreter.eval_modules(&Some(input), enable_tracing)?;
|
|
|
|
// Now eval the query.
|
|
results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?);
|
|
}
|
|
} 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.
|
|
results.push(interpreter.eval_query_snippet(&expr, enable_tracing)?);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "intended for use by scripts/rego-eval"]
|
|
fn one_file() -> Result<()> {
|
|
env_logger::init();
|
|
|
|
let mut file = String::default();
|
|
let mut input = None;
|
|
for a in env::args() {
|
|
if a.ends_with(".rego") {
|
|
file = a;
|
|
} else if a.ends_with(".json") {
|
|
let input_json = std::fs::read_to_string(&a)?;
|
|
let value = Value::from_json_str(input_json.as_str())?;
|
|
input = Some(value);
|
|
}
|
|
}
|
|
|
|
if file.is_empty() {
|
|
bail!("missing <policy.rego>");
|
|
}
|
|
|
|
let contents = std::fs::read_to_string(&file)?;
|
|
|
|
let source = Source {
|
|
file: file.as_str(),
|
|
contents: contents.as_str(),
|
|
lines: contents.split('\n').collect(),
|
|
};
|
|
let mut parser = Parser::new(&source)?;
|
|
let modules = vec![parser.parse()?];
|
|
|
|
let analyzer = Analyzer::new();
|
|
let schedule = analyzer.analyze(&modules)?;
|
|
|
|
let mut modules_ref = vec![];
|
|
for m in &modules {
|
|
modules_ref.push(m);
|
|
}
|
|
|
|
let mut interpreter = interpreter::Interpreter::new(modules_ref)?;
|
|
interpreter.prepare_for_eval(Some(&schedule), &None)?;
|
|
let results = interpreter.eval_modules(&input, true)?;
|
|
println!("eval results:\n{}", serde_json::to_string_pretty(&results)?);
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
pub enum ValueOrVec {
|
|
Single(Value),
|
|
Many(Vec<Value>),
|
|
}
|
|
|
|
impl Serialize for ValueOrVec {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
match self {
|
|
ValueOrVec::Single(value) => value.serialize(serializer),
|
|
ValueOrVec::Many(v) => {
|
|
let mut map = serializer.serialize_map(Some(1))?;
|
|
map.serialize_entry("many!", v)?;
|
|
map.end()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for ValueOrVec {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let value = Value::deserialize(deserializer)?;
|
|
|
|
match &value["many!"] {
|
|
Value::Array(arr) => Ok(ValueOrVec::Many(arr.to_vec())),
|
|
_ => Ok(ValueOrVec::Single(value)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct TestCase {
|
|
data: Value,
|
|
input: Option<ValueOrVec>,
|
|
modules: Vec<String>,
|
|
note: String,
|
|
query: String,
|
|
sort_bindings: Option<bool>,
|
|
want_result: Option<ValueOrVec>,
|
|
skip: Option<bool>,
|
|
error: Option<String>,
|
|
traces: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
|
struct YamlTest {
|
|
cases: Vec<TestCase>,
|
|
}
|
|
|
|
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)?;
|
|
|
|
println!("running {file}");
|
|
|
|
for case in test.cases {
|
|
print!("case {} ", case.note);
|
|
if case.skip == Some(true) {
|
|
println!("skipped");
|
|
continue;
|
|
}
|
|
|
|
match (&case.want_result, &case.error) {
|
|
(Some(_), None) | (None, Some(_)) => (),
|
|
_ => panic!("either want_result or error must be specified in test case."),
|
|
}
|
|
|
|
let enable_tracing = case.traces.is_some() && case.traces.unwrap();
|
|
|
|
match eval_file(
|
|
&case.modules,
|
|
Some(case.data),
|
|
case.input,
|
|
case.query.as_str(),
|
|
enable_tracing,
|
|
) {
|
|
Ok(results) => match case.want_result {
|
|
Some(want_result) => {
|
|
let mut expected_results = vec![];
|
|
match want_result {
|
|
ValueOrVec::Single(single_result) => expected_results.push(single_result),
|
|
ValueOrVec::Many(mut many_result) => {
|
|
expected_results.append(&mut many_result)
|
|
}
|
|
}
|
|
|
|
assert_match(results, expected_results);
|
|
}
|
|
_ => panic!("eval succeeded and did not produce any errors"),
|
|
},
|
|
Err(actual) => match &case.error {
|
|
Some(expected) => {
|
|
let actual = actual.to_string();
|
|
if !actual.contains(expected) {
|
|
bail!(
|
|
"Error message\n`{}\n`\ndoes not contain `{}`",
|
|
actual,
|
|
expected
|
|
);
|
|
}
|
|
println!("{actual}");
|
|
}
|
|
_ => return Err(actual),
|
|
},
|
|
}
|
|
|
|
println!("passed");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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.
|
|
// Therefore, panic with the error.
|
|
panic!("{}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn yaml_test_basic() -> Result<()> {
|
|
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();
|
|
for a in env::args() {
|
|
if a.ends_with(".yaml") {
|
|
file = a;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if file.is_empty() {
|
|
bail!("missing <policy.rego>");
|
|
}
|
|
|
|
yaml_test(file.as_str())
|
|
}
|
|
|
|
#[test_resources("tests/interpreter/**/*.yaml")]
|
|
fn run(path: &str) {
|
|
yaml_test(path).unwrap()
|
|
}
|