mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
- ignore worktrees - feature guard time module - Apply with modifiers before evaluating loop expressions - Support value modifier for functions - stubs for http.send and io.jwt.decode_verify - Initialize with-document after initializing init data - In case of conflict, with modifier override init-data values. - In case of conflict, subsequent with modifier overrides earlier ones. - Ensure that zero parameter functions are evaluated and added to document - opa.runtime builtin returns: - git commit hash - environment vars - regorus features enabled - builtins available - deprecated builtins available - If `sort_bindings` is specified, sort the bindings in OPA tests - gather inputs, used vars and comprehensions in with modifiers - For refs starting with `data`, ensure that modules are evaluated before looking up value of the expression. Thie ensures that modules that have only been partly populated (E.g via with mods) are completely evaluated before the value is looked up - Mark rules overridden using with modifiers are evaluated. - Exclude env vars in opa.runtime. - Include regorus version in OPA runtime - update to opa v0.60.0 - scheduler: Handle function refs in with modifers. Error out only if a truly undefined ref. - Handle undefined params, parameter expression evaluation errors before applying with modifiers. - When applying with modifiers, first determine whether the target is a function. If so, handle cleanly. - concat: raise error only in strict mode - In strict mode, propagate errors raised by function rule execution in case of multiple function definitions for same rule - skip "withkeyword/builtin-builtin: arity 0" test which can never pass. - When a mock has is being applied, clear with_function so that other mocks won't be applied during the evaluation of the mock. - Ability to specify strictness in tests Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
357 lines
10 KiB
Rust
357 lines
10 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
use std::env;
|
|
|
|
use anyhow::{bail, Result};
|
|
use regorus::*;
|
|
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
|
use test_generator::test_resources;
|
|
|
|
// 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.as_ref() == "#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 match_values(computed: &Value, expected: &Value) -> Result<()> {
|
|
if computed != expected {
|
|
panic!(
|
|
"{}",
|
|
colored_diff::PrettyDifference {
|
|
expected: &serde_yaml::to_string(&expected)?,
|
|
actual: &serde_yaml::to_string(&computed)?
|
|
}
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> {
|
|
if computed_results.len() != expected_results.len() {
|
|
dbg!((&computed_results, &expected_results));
|
|
bail!(
|
|
"the number of computed results ({}) and expected results ({}) is not equal",
|
|
computed_results.len(),
|
|
expected_results.len()
|
|
);
|
|
}
|
|
|
|
for (n, expected_result) in expected_results.iter().enumerate() {
|
|
let expected = match process_value(expected_result) {
|
|
Ok(e) => e,
|
|
_ => bail!("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) => bail!("{e}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn push_query_results(query_results: QueryResults, results: &mut Vec<Value>) {
|
|
if query_results.result.len() == 1 {
|
|
if let Some(query_result) = query_results.result.last() {
|
|
if !query_result.bindings.is_empty_object() {
|
|
results.push(query_result.bindings.clone());
|
|
} else {
|
|
for e in query_result.expressions.iter() {
|
|
results.push(e.value.clone());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for r in query_results.result.iter() {
|
|
if !r.bindings.is_empty_object() {
|
|
results.push(r.bindings.clone());
|
|
} else {
|
|
results.push(Value::from_array(
|
|
r.expressions.iter().map(|e| e.value.clone()).collect(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn eval_file(
|
|
regos: &[String],
|
|
data_opt: Option<Value>,
|
|
input_opt: Option<ValueOrVec>,
|
|
query: &str,
|
|
enable_tracing: bool,
|
|
strict: bool,
|
|
) -> Result<Vec<Value>> {
|
|
let mut engine: Engine = engine::Engine::new();
|
|
engine.set_strict_builtin_errors(strict);
|
|
|
|
let mut results = vec![];
|
|
let mut files = 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();
|
|
engine.add_policy(file.to_string(), contents.to_string())?;
|
|
}
|
|
|
|
if let Some(data) = data_opt {
|
|
engine.add_data(data)?;
|
|
}
|
|
|
|
if let Some(input) = input_opt {
|
|
// 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 {
|
|
engine.set_input(input);
|
|
engine.eval_modules(enable_tracing)?;
|
|
|
|
// Now eval the query.
|
|
push_query_results(
|
|
engine.eval_query(query.to_string(), enable_tracing)?,
|
|
&mut results,
|
|
);
|
|
}
|
|
} else {
|
|
// it no input is defined then one evaluation of all modules is performed
|
|
// Now eval the query.
|
|
push_query_results(
|
|
engine.eval_query(query.to_string(), enable_tracing)?,
|
|
&mut results,
|
|
);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
#[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: Option<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>,
|
|
want_error: Option<String>,
|
|
want_error_code: Option<String>,
|
|
#[serde(default = "default_strict")]
|
|
strict: bool,
|
|
}
|
|
|
|
fn default_strict() -> bool {
|
|
true
|
|
}
|
|
|
|
#[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,
|
|
case.data,
|
|
case.input,
|
|
case.query.as_str(),
|
|
enable_tracing,
|
|
case.strict,
|
|
) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
check_output(&results, &expected_results)?;
|
|
}
|
|
_ => bail!("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;
|
|
}
|
|
}
|
|
|
|
if file.is_empty() {
|
|
bail!("missing <yaml-file>");
|
|
}
|
|
|
|
yaml_test(file.as_str())
|
|
}
|
|
|
|
#[test_resources("tests/interpreter/**/*.yaml")]
|
|
fn run(path: &str) {
|
|
yaml_test(path).unwrap()
|
|
}
|