use anyhow::{anyhow, bail, Result};
use regorus::{unstable::*, *};
use serde::{Deserialize, Serialize};
use test_generator::test_resources;
macro_rules! my_assert_eq {
($left:expr, $right:expr, $($arg:tt)+) => {
match (&($left), &($right)) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
return Err(anyhow!("mismatch:\nleft = {}\nright = {}\n{}",
&$left, &$right, format_args!($($arg)+)));
}
}
}
}
}
fn skip_value(v: &Value) -> bool {
matches!(v, Value::String(s) if s.as_ref() == "--skip--")
}
fn match_span(s: &Span, v: &Value) -> Result<()> {
match &v {
Value::String(vs) => {
my_assert_eq!(
s.text(),
vs.as_ref(),
"{}",
s.source
.message(s.line, s.col, "match-error", "mismatch happened here.")
);
}
_ => {
my_assert_eq!(
*s.text(),
serde_json::to_string_pretty(v)?,
"{}",
s.source
.message(s.line, s.col, "match-error", "mismatch happened here.")
)
}
}
Ok(())
}
fn match_span_opt(s: &Span, v: &Value) -> Result<()> {
if *v != Value::Undefined {
match_span(s, v)
} else {
Ok(())
}
}
fn match_vec(s: &Span, vec: &Vec<Ref<Expr>>, v: &Value) -> Result<()> {
if v.as_object().is_ok() {
match_span_opt(s, &v["span"])?;
return match_vec(s, vec, &v["values"]);
}
let v = v.as_array()?;
my_assert_eq!(
vec.len(),
v.len(),
"{}",
s.source.message(
s.line,
s.col,
"match-error",
"mismatch in number of elements in sequence following this location"
)
);
for i in 0..vec.len() {
match_expr(&vec[i], &v[i])?;
}
Ok(())
}
fn match_object(s: &Span, fields: &[(Span, Ref<Expr>, Ref<Expr>)], v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match_span_opt(s, &v["span"])?;
match &v["fields"].as_array() {
Ok(a) => {
my_assert_eq!(fields.len(), a.len(), "field length mismatch");
for (idx, (_, k, v)) in fields.iter().enumerate() {
match_expr(k, &a[idx]["key"])?;
match_expr(v, &a[idx]["value"])?;
}
Ok(())
}
_ => bail!("incorrect field specification in yaml. Must be array."),
}
}
fn match_expr_impl(e: &Expr, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match e {
Expr::String { span: s, .. } => match_span(s, &v["string"]),
Expr::RawString { span: s, .. } => match_span(s, &v["rawstring"]),
Expr::Number { span: s, .. } => match_span(s, &v["number"]),
Expr::Bool { span: s, .. } => match_span(s, &v["bool"]),
Expr::Null { span: s, .. } => match_span(s, &v["null"]),
Expr::Var { span: s, .. } => match_span(s, &v["var"]),
Expr::Array { span, items, .. } => match_vec(span, items, &v["array"]),
Expr::Set { span, items, .. } => match_vec(span, items, &v["set"]),
Expr::Object { span, fields, .. } => match_object(span, fields, &v["object"]),
Expr::ArrayCompr {
span, term, query, ..
} => {
match_span_opt(span, &v["arraycompr"]["span"])?;
match_expr(term, &v["arraycompr"]["term"])?;
match_query(query, &v["arraycompr"]["query"])
}
Expr::SetCompr {
span, term, query, ..
} => {
match_span_opt(span, &v["setcompr"]["span"])?;
match_expr(term, &v["setcompr"]["term"])?;
match_query(query, &v["setcompr"]["query"])
}
Expr::ObjectCompr {
span,
key,
value,
query,
..
} => {
match_span_opt(span, &v["objectcompr"]["span"])?;
match_expr(key, &v["objectcompr"]["key"])?;
match_expr(value, &v["objectcompr"]["value"])?;
match_query(query, &v["objectcompr"]["query"])
}
Expr::Call {
span, fcn, params, ..
} => {
match_span_opt(span, &v["call"]["span"])?;
match_expr(fcn, &v["call"]["fcn"])?;
match_vec(span , params, &v["call"]["params"])
}
Expr::RefDot {
span, refr, field, ..
} => {
match_span_opt(span, &v["refdot"]["span"])?;
match_expr(refr, &v["refdot"]["refr"])?;
match_span(&field.0, &v["refdot"]["field"])
}
Expr::RefBrack {
span, refr, index, ..
} => {
match_span_opt(span, &v["refbrack"]["span"])?;
match_expr(refr, &v["refbrack"]["refr"])?;
match_expr(index, &v["refbrack"]["index"])
}
Expr::UnaryExpr { span, expr, .. } => {
match_span_opt(span, &v["span"])?;
my_assert_eq!(
&Value::String("-".into()),
&v["op"],
"{}",
span.source.message(
span.line,
span.col,
"mismatch-error",
"could not match `-` operator",
),
);
match_expr(expr, &v["expr"])
}
Expr::BinExpr {
span, op, lhs, rhs, ..
} => {
match_span_opt(span, &v["binexpr"]["span"])?;
match_bin_op(span, op, &v["binexpr"]["op"])?;
match_expr(lhs, &v["binexpr"]["lhs"])?;
match_expr(rhs, &v["binexpr"]["rhs"])
}
Expr::ArithExpr {
span, op, lhs, rhs, ..
} => {
match_span_opt(span, &v["arithexpr"]["span"])?;
match_arith_op(span, op, &v["arithexpr"]["op"])?;
match_expr(lhs, &v["arithexpr"]["lhs"])?;
match_expr(rhs, &v["arithexpr"]["rhs"])
}
Expr::BoolExpr {
span, op, lhs, rhs, ..
} => {
match_span_opt(span, &v["boolexpr"]["span"])?;
match_bool_op(span, op, &v["boolexpr"]["op"])?;
match_expr(lhs, &v["boolexpr"]["lhs"])?;
match_expr(rhs, &v["boolexpr"]["rhs"])
}
Expr::AssignExpr {
span, op, lhs, rhs, ..
} => {
match_span_opt(span, &v["assignexpr"]["span"])?;
match_assign_op(span, op, &v["assignexpr"]["op"])?;
match_expr(lhs, &v["assignexpr"]["lhs"])?;
match_expr(rhs, &v["assignexpr"]["rhs"])
}
Expr::Membership {
span,
key,
value,
collection,
..
} => {
match_span_opt(span, &v["inexpr"]["span"])?;
match_expr_opt(span, key, &v["inexpr"]["key"])?;
match_expr(value, &v["inexpr"]["value"])?;
match_expr(collection, &v["inexpr"]["collection"])
}
#[cfg(feature = "rego-extensions")]
Expr::OrExpr { span, lhs, rhs, .. } => {
match_span_opt(span, &v["orexpr"]["span"])?;
match_expr(lhs, &v["orexpr"]["lhs"])?;
match_expr(rhs, &v["orexpr"]["rhs"])
}
}?;
match (e.eidx(), &v["eidx"]) {
(eidx, Value::Number(n)) if n.as_u64() == Some(eidx as u64) => Ok(()),
_ => {
bail!(
"{}",
e.span().message(
"mismatch-error",
format!(
"eidx mismatch: expected {:?}, got {:?}",
v["eidx"],
e.eidx(),
)
.as_str()
)
);
}
}
}
#[allow(clippy::let_and_return)]
fn match_expr(expr: &Expr, v: &Value) -> Result<()> {
let r = match_expr_impl(expr, v);
r
}
fn match_with_mod(m: &WithModifier, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match_span_opt(&m.span, &v["span"])?;
match_expr(&m.refr, &v["refr"])?;
match_expr(&m.r#as, &v["as"])
}
fn match_literal_stmt(ls: &LiteralStmt, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match &v["sidx"] {
Value::Number(n) if n.as_u64() == Some(ls.sidx as u64) => (),
_ => {
bail!(
"{}",
ls.span.message(
"mismatch-error",
format!("sidx mismatch: expected {:?}, got {:?}", v["sidx"], ls.sidx).as_str()
)
);
}
}
match_span_opt(&ls.span, &v["span"])?;
match_literal(&ls.literal, &v["literal"])?;
let with_mods = &v["with-mods"];
if skip_value(with_mods) {
return Ok(());
}
match with_mods.as_array() {
Ok(a) => {
my_assert_eq!(
ls.with_mods.len(),
a.len(),
"{}",
ls.span.source.message(
ls.span.line,
ls.span.col,
"mismatch-error",
"with-modifier count mismatch"
)
);
for (idx, with_mod) in a.iter().enumerate() {
match_with_mod(&ls.with_mods[idx], with_mod)?;
}
}
_ if ls.with_mods.is_empty() => (),
_ => {
bail!(
"{}",
ls.span.source.message(
ls.span.line,
ls.span.col,
"mismatch-error",
"failed to match with-modifiers"
)
)
}
}
Ok(())
}
fn match_query(q: &Query, v: &Value) -> Result<()> {
match &v["qidx"] {
Value::Number(n) if n.as_u64() == Some(q.qidx as u64) => (),
_ => {
bail!(
"{}",
q.span.message(
"mismatch-error",
format!("qidx mismatch: expected {:?}, got {:?}", v["qidx"], q.qidx).as_str()
)
);
}
}
match_span_opt(&q.span, &v["span"])?;
let stmts = &v["stmts"].as_array();
let stmts = match &stmts {
Ok(s) => s,
_ => {
bail!(
"{}",
q.span.source.message(
q.span.line,
q.span.col,
"mismatch-error",
"empty statements list in query specified"
)
)
}
};
my_assert_eq!(
q.stmts.len(),
stmts.len(),
"{}",
q.span.source.message(
q.span.line,
q.span.col,
"mismatch-error",
"mismatch in statement count"
)
);
for (idx, stmt) in stmts.iter().enumerate() {
match_literal_stmt(&q.stmts[idx], stmt)?;
}
Ok(())
}
fn match_expr_opt(s: &Span, e: &Option<Ref<Expr>>, v: &Value) -> Result<()> {
match (e, v) {
(Some(e), v) => match_expr(e, v),
(None, Value::Undefined) => Ok(()),
_ => {
bail!(
"{}",
s.source.message(
s.line,
s.col,
"mismatch-error",
format!(
"failed to match {:#?} and {}",
e,
serde_json::to_string_pretty(&v)?
)
.as_str()
)
)
}
}
}
fn match_bin_op(s: &Span, op: &BinOp, v: &Value) -> Result<()> {
match (op, v) {
(BinOp::Intersection, Value::String(s)) if s.as_ref() == "&" => Ok(()),
(BinOp::Union, Value::String(s)) if s.as_ref() == "|" => Ok(()),
_ => bail!(
"{}",
s.source.message(
s.line,
s.col,
"mismatch-error",
format!("left = {op:?}\nright = {v:?}\n").as_str()
)
),
}
}
fn match_arith_op(s: &Span, op: &ArithOp, v: &Value) -> Result<()> {
match (op, v) {
(ArithOp::Add, Value::String(s)) if s.as_ref() == "+" => Ok(()),
(ArithOp::Sub, Value::String(s)) if s.as_ref() == "-" => Ok(()),
(ArithOp::Mul, Value::String(s)) if s.as_ref() == "*" => Ok(()),
(ArithOp::Div, Value::String(s)) if s.as_ref() == "/" => Ok(()),
_ => bail!(
"{}",
s.source.message(
s.line,
s.col,
"mismatch-error",
format!("left = {op:?}\nright = {v:?}\n").as_str()
)
),
}
}
fn match_bool_op(s: &Span, op: &BoolOp, v: &Value) -> Result<()> {
match (op, v) {
(BoolOp::Lt, Value::String(s)) if s.as_ref() == "<" => Ok(()),
(BoolOp::Le, Value::String(s)) if s.as_ref() == "<=" => Ok(()),
(BoolOp::Eq, Value::String(s)) if s.as_ref() == "==" => Ok(()),
(BoolOp::Ge, Value::String(s)) if s.as_ref() == ">=" => Ok(()),
(BoolOp::Gt, Value::String(s)) if s.as_ref() == ">" => Ok(()),
_ => bail!(
"{}",
s.source.message(
s.line,
s.col,
"mismatch-error",
format!("left = {op:?}\nright = {v:?}\n").as_str()
)
),
}
}
fn match_assign_op(s: &Span, op: &AssignOp, v: &Value) -> Result<()> {
match (op, v) {
(AssignOp::Eq, Value::String(s)) if s.as_ref() == "=" => Ok(()),
(AssignOp::ColEq, Value::String(s)) if s.as_ref() == ":=" => Ok(()),
_ => bail!(
"{}",
s.source.message(
s.line,
s.col,
"mismatch-error",
format!("left = {op:?}\nright = {v:?}\n").as_str()
)
),
}
}
fn match_rule_assign(a: &RuleAssign, v: &Value) -> Result<()> {
match_span_opt(&a.span, &v["span"])?;
match_assign_op(&a.span, &a.op, &v["op"])?;
match_expr(&a.value, &v["value"])
}
fn match_rule_assign_opt(a: &Option<RuleAssign>, v: &Value) -> Result<()> {
match a {
Some(a) => match_rule_assign(a, v),
None => {
my_assert_eq!(*v, Value::Undefined, "mismatch in null assign");
Ok(())
}
}
}
fn match_rule_head(h: &RuleHead, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match h {
RuleHead::Compr { span, refr, assign } => {
match_span_opt(span, &v["compr"]["span"])?;
match_expr(refr, &v["compr"]["refr"])?;
match_rule_assign_opt(assign, &v["compr"]["assign"])
}
RuleHead::Set { span, refr, key } => {
match_span_opt(span, &v["set"]["span"])?;
match_expr(refr, &v["set"]["refr"])?;
match_expr_opt(span, key, &v["set"]["key"])
}
RuleHead::Func {
span,
refr,
args,
assign,
} => {
match_span_opt(span, &v["func"]["span"])?;
match_expr(refr, &v["func"]["refr"])?;
match_vec(span , args, &v["func"]["args"])?;
match_rule_assign_opt(assign, &v["func"]["assign"])
}
}
}
fn match_literal(l: &Literal, v: &Value) -> Result<()> {
match l {
Literal::SomeVars { span, vars } => {
let v = &v["some-vars"];
match_span_opt(span, &v["span"])?;
let values = &v["vars"].as_array()?;
my_assert_eq!(
vars.len(),
values.len(),
"some-vars mismatch {:#?} {}",
vars,
serde_json::to_string_pretty(&values)?
);
for idx in 0..vars.len() {
match_span(&vars[idx], &values[idx])?
}
Ok(())
}
Literal::SomeIn {
span,
key,
value,
collection,
} => {
let v = &v["some-decl"];
match_span_opt(span, &v["span"])?;
match_expr(value, &v["value"])?;
match_expr_opt(span, key, &v["key"])?;
match_expr(collection, &v["collection"])
}
Literal::Expr { expr, .. } => match_expr(expr, &v["expr"]),
Literal::NotExpr { expr, span } => {
let v = &v["notexpr"];
match &v["op"] {
Value::String(s) if s.as_ref() == "not" => (),
_ => {
bail!(
"{}",
span.source.message(
span.line,
span.col,
"mismatch-error",
"`op: -` not found in value`"
)
)
}
}
match_expr(expr, v)
}
Literal::Every {
span,
key,
value,
domain,
query,
} => {
match_span_opt(span, &v["every"]["span"])?;
match_span(value, &v["every"]["value"])?;
match key {
Some(s) => match_span(s, &v["every"]["key"])?,
None => {
my_assert_eq!(
&Value::Undefined,
&v["key"],
"{}",
span.source.message(
span.line,
span.col,
"mismatch-error",
"could not match `key``"
)
);
}
}
match_expr(domain, &v["every"]["domain"])?;
match_query(query, &v["every"]["query"])
}
}
}
fn match_rule_body(b: &RuleBody, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match_span_opt(&b.span, &v["span"])?;
match_rule_assign_opt(&b.assign, &v["assign"])?;
match_query(&b.query, &v["query"])
}
fn match_rule_bodies(span: &Span, bodies: &[RuleBody], v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
let v = &v.as_array();
let v = match &v {
Ok(v) => v,
_ => {
bail!(
"incorrect yaml. bodies is not an array. Corresponding rego: {}",
span.source.message(span.line, span.col, "invalid-yaml", "")
);
}
};
my_assert_eq!(
bodies.len(),
v.len(),
"{}",
span.source.message(
span.line,
span.col,
"mismatch-error",
"mismatch in body count",
),
);
for idx in 0..bodies.len() {
match_rule_body(&bodies[idx], &v[idx])?;
}
Ok(())
}
fn match_rule(r: &Rule, v: &Value) -> Result<()> {
match r {
Rule::Spec { span, head, bodies } => {
let obj = &v["spec"];
match_span_opt(span, &obj["span"])?;
match_rule_head(head, &obj["head"])?;
match_rule_bodies(span, bodies, &obj["bodies"])
}
Rule::Default {
span,
refr,
args,
op,
value,
} => {
let obj = &v["default"];
match_span_opt(span, &obj["span"])?;
match_expr(refr, &obj["refr"])?;
match_vec(span , args, &obj["args"])?;
match_assign_op(span, op, &obj["op"])?;
match_expr(value, &obj["value"])
}
}
}
fn match_package(p: &Package, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match_span_opt(&p.span, &v["span"])?;
match_expr(&p.refr, &v["refr"])
}
fn match_import(i: &Import, v: &Value) -> Result<()> {
if skip_value(v) {
return Ok(());
}
match_span_opt(&i.span, &v["span"])?;
match_expr(&i.refr, &v["refr"])?;
match (&i.r#as, &v["as"]) {
(Some(a), v) => match_span(a, v),
(None, Value::Undefined) => Ok(()),
_ => Err(i
.span
.source
.error(i.span.line, i.span.col, "import does not have `as` binding")),
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct TestCase {
rego: String,
note: String,
package: Option<Value>,
imports: Option<Vec<Value>>,
policy: Option<Vec<Value>>,
error: Option<String>,
#[serde(default)]
num_expressions: u32,
#[serde(default)]
num_statements: u32,
#[serde(default)]
num_queries: u32,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct YamlTest {
cases: Vec<TestCase>,
}
fn yaml_test_impl(file: &str) -> Result<()> {
println!("\nrunning {file}");
let yaml_str = std::fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
for case in &test.cases {
print!("\ncase {} ", case.note);
let source = Source::from_contents("case.rego".to_string(), case.rego.clone())?;
let mut parser = Parser::new(&source)?;
match parser.parse() {
Ok(module) => {
if let Some(e) = &case.error {
bail!("error `{}` not raised by parser.", e);
}
my_assert_eq!(
module.num_expressions,
case.num_expressions,
"mismatch in num_expressions"
);
my_assert_eq!(
module.num_statements,
case.num_statements,
"mismatch in num_statements"
);
my_assert_eq!(
module.num_queries,
case.num_queries,
"mismatch in num_queries"
);
if let Some(p) = &case.package {
match_package(&module.package, p)?;
}
if let Some(imports) = &case.imports {
my_assert_eq!(
module.imports.len(),
imports.len(),
"mismatch in number of imports"
);
for (idx, import) in imports.iter().enumerate().take(module.imports.len()) {
match_import(&module.imports[idx], import)?;
}
}
if let Some(policy) = &case.policy {
my_assert_eq!(
module.policy.len(),
policy.len(),
"mismatch in policy length"
);
for (idx, policy) in policy.iter().enumerate().take(module.policy.len()) {
if skip_value(policy) {
continue;
}
match_rule(&module.policy[idx], policy)?;
}
}
}
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");
}
println!("{} cases passed.", test.cases.len());
Ok(())
}
fn yaml_test(file: &str) -> Result<()> {
match yaml_test_impl(file) {
Ok(_) => Ok(()),
Err(e) => {
panic!("{}", e);
}
}
}
#[test_resources("tests/parser/**/*.yaml")]
fn run(path: &str) {
yaml_test(path).unwrap()
}