mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat!: Rego -> RVM Compiler and extensive testsuite (#506)
# RVM compiler test cases Coverage: - arithmetic - arrays - chained lookups - comparisons - comprehensions - default rules - destructuring - function rules - loops/quantifiers - multiple entrypoints - objects/sets - variables - negative/edge scenarios such as data/rule conflicts - virtual data lookups - etc # Modify interpreter and compiled policy for RVM Compilation - Interpreter::eval_default_rule_for_compiler: evaluates a named default rule in isolation - allows compiler to emit a constant value instead of instructions for the default value # feat: Rego Compiler Scaffolding - Introduce the rego::compiler module surface and entry point wiring - Add the core compiler concepts: - register allocator - scope tracking - literal/builtin tables - rule worklists - instruction emit helpers - compiler-specific error types - context structs for rules, comprehensions, and loops to support later lowering passes. # feat: Compile Rules/Queries - add compiler::compile_from_policy workflow plus rule worklist, entry-point wiring, and recursion checks - implement query lowering: - scheduling-aware statement ordering - loop hoisting - “every/some” semantics - context yields - literal assertions - finalize Program construction # feat: Expression Lowering - add compile_rego_expr and helpers to translate every AST expression into RVM instructions, - interop with binding plans, comprehensions, and membership checks. - implement collection literal builders (ArrayCreate, SetCreate, ObjectCreate) - dedupe literal keys and handle mixed literal/dynamic fields via instruction data blocks. - operations: - arithmetic/boolean/bin operators - membership - unary minus - set unions/intersections - etc - user-defined and builtin function calls - reference handling - analyse chained refs - distinguishe data/input/local roots - perform rule dispatch or virtual document lookups - emits optimized Index/ChainedIndex instructions. # feat: Comprehensions & Loops - shared comprehension emitter - wraps array/set/object comprehensions with ComprehensionBegin/End - context management - loop lowering utilities - read hoisting metadata - emit LoopStart/LoopNext - some in lowering - every quantifiers - index iteration - propagate binding plans into stored registers so downstream statements see bound variables. # feat: Destructuring Lowering - destructuring planner integration - assignment/parameter/loop bindings use hoisted plans instead of re-walking ASTs. - handle :=, =, wildcard matches, and equality - evaluate RHS - applying destructuring plans - emit assert condition as needed - support nested array/object destructuring, dynamic keys, and some ... in forms # test: Shared Testing + RVM Suites - move YAML test helpers into test_utils.rs and re-export via common.rs for use by interpreter and vm test suites - comprehensive compiler test suite - compiles policies with the new Rego→RVM compiler - runs them through RegoVM - compares against interpreter behavior - supports multiple entry points - provides assembly listings - filterable YAML suites. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
688e6128d4
commit
a3a20a1235
@@ -8,3 +8,6 @@ mod engine;
|
||||
mod lexer;
|
||||
mod parser;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "rvm")]
|
||||
mod rvm;
|
||||
|
||||
3
tests/rvm/mod.rs
Normal file
3
tests/rvm/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
mod rego;
|
||||
50
tests/rvm/rego/cases/arithmetic.yaml
Normal file
50
tests/rvm/rego/cases/arithmetic.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Arithmetic Operations Test Suite
|
||||
# Tests basic arithmetic operations: addition, multiplication, division, subtraction
|
||||
|
||||
cases:
|
||||
- note: arithmetic_basic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := 2 + 3
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 5
|
||||
|
||||
- note: arithmetic_multiply
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := 4 * 6
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 24
|
||||
|
||||
- note: arithmetic_division
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := 15 / 3
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 5
|
||||
|
||||
- note: arithmetic_subtraction
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := 10 - 7
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 3
|
||||
64
tests/rvm/rego/cases/arrays.yaml
Normal file
64
tests/rvm/rego/cases/arrays.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Arrays Test Suite
|
||||
# Tests array creation, nested arrays, indexing, and mixed data structures
|
||||
|
||||
cases:
|
||||
- note: array_creation
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := [1, 2, 3, "hello", true]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [1, 2, 3, "hello", true]
|
||||
|
||||
- note: nested_arrays
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := [[1, 2], [3, 4], ["a", "b"]]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [[1, 2], [3, 4], ["a", "b"]]
|
||||
|
||||
- note: array_indexing
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
arr := [10, 20, 30, 40]
|
||||
main := result if {
|
||||
result := arr[2]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 30
|
||||
|
||||
- note: dynamic_array_indexing
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
arr := ["first", "second", "third"]
|
||||
index := 1
|
||||
main := result if {
|
||||
result := arr[index]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "second"
|
||||
|
||||
- note: mixed_array_object
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := [{"name": "Alice"}, {"name": "Bob"}, [1, 2, 3]]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [{"name": "Alice"}, {"name": "Bob"}, [1, 2, 3]]
|
||||
293
tests/rvm/rego/cases/chained_access.yaml
Normal file
293
tests/rvm/rego/cases/chained_access.yaml
Normal file
@@ -0,0 +1,293 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Chained Access and Variable Resolution Test Suite
|
||||
# Tests complex chained reference expressions, dynamic indexing, and variable precedence
|
||||
|
||||
cases:
|
||||
- note: simple_data_rule_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.users
|
||||
alice = {"name": "Alice", "age": 30}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.users.alice.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice"
|
||||
|
||||
- note: local_variable_precedence_over_rule
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
alice = {"name": "Global Alice"}
|
||||
main := result if {
|
||||
alice := {"name": "Local Alice"}
|
||||
result := alice.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Local Alice"
|
||||
|
||||
- note: chained_rule_access_with_fields
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.auth
|
||||
user_permissions = {
|
||||
"alice": {"read": true, "write": false, "admin": false},
|
||||
"bob": {"read": true, "write": true, "admin": true}
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.auth.user_permissions.alice.read
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: dynamic_indexing_with_variable
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.data
|
||||
users = {
|
||||
"alice": {"name": "Alice Smith", "role": "user"},
|
||||
"bob": {"name": "Bob Jones", "role": "admin"}
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
user_id := "alice"
|
||||
result := data.test.data.users[user_id].name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice Smith"
|
||||
|
||||
- note: mixed_static_and_dynamic_chaining
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.config
|
||||
settings = {
|
||||
"databases": {
|
||||
"primary": {"host": "db1.example.com", "port": 5432},
|
||||
"backup": {"host": "db2.example.com", "port": 5433}
|
||||
}
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
db_type := "primary"
|
||||
result := data.test.config.settings.databases[db_type].host
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "db1.example.com"
|
||||
|
||||
- note: input_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := input.user.profile.email
|
||||
}
|
||||
query: data.test.main
|
||||
input: {"user": {"profile": {"email": "alice@example.com", "verified": true}}}
|
||||
want_result: "alice@example.com"
|
||||
|
||||
- note: dynamic_input_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
field := "email"
|
||||
result := input.user.profile[field]
|
||||
}
|
||||
query: data.test.main
|
||||
input: {"user": {"profile": {"email": "alice@example.com", "phone": "+1234567890"}}}
|
||||
want_result: "alice@example.com"
|
||||
|
||||
- note: data_document_with_rule_override
|
||||
data: {"test": {"existing": {"value": "from_data"}}}
|
||||
modules:
|
||||
- |
|
||||
package test.existing
|
||||
computed = "from_rule"
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := [data.test.existing.value, data.test.existing.computed]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: ["from_data", "from_rule"]
|
||||
|
||||
- note: longest_rule_prefix_matching
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.api.v1
|
||||
users = ["alice", "bob"]
|
||||
- |
|
||||
package test.api.v1.users_pkg
|
||||
count = 2
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := [data.test.api.v1.users, data.test.api.v1.users_pkg.count]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [["alice", "bob"], 2]
|
||||
|
||||
- note: nested_dynamic_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.complex
|
||||
matrix = {
|
||||
"level1": {
|
||||
"level2a": {"value": "found_a"},
|
||||
"level2b": {"value": "found_b"}
|
||||
}
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
level1_key := "level1"
|
||||
level2_key := "level2a"
|
||||
result := data.test.complex.matrix[level1_key][level2_key].value
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "found_a"
|
||||
|
||||
- note: variable_shadowing_in_chain
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
config = {"timeout": 30}
|
||||
main := result if {
|
||||
config := {"nested": {"timeout": 60}}
|
||||
result := config.nested.timeout
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 60
|
||||
|
||||
- note: array_indexing_in_chain
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.data
|
||||
servers = [
|
||||
{"name": "web1", "status": "active"},
|
||||
{"name": "web2", "status": "inactive"},
|
||||
{"name": "db1", "status": "active"}
|
||||
]
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
index := 0
|
||||
result := data.test.data.servers[index].name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "web1"
|
||||
|
||||
- note: string_literal_bracket_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.metrics
|
||||
cpu_usage = {
|
||||
"server-1": 45.2,
|
||||
"server-2": 78.9,
|
||||
"load-balancer": 12.3
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.metrics.cpu_usage["server-1"]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 45.2
|
||||
|
||||
- note: complex_nested_rule_resolution
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.auth.policies
|
||||
admin_policy = {
|
||||
"permissions": ["read", "write", "delete"],
|
||||
"resources": ["users", "configs", "logs"]
|
||||
}
|
||||
- |
|
||||
package test.auth.config
|
||||
max_sessions = 5
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
perms := data.test.auth.policies.admin_policy.permissions
|
||||
max_sess := data.test.auth.config.max_sessions
|
||||
result := {"permissions": perms, "max_sessions": max_sess}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"permissions": ["read", "write", "delete"], "max_sessions": 5}
|
||||
|
||||
- note: undefined_chain_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.nonexistent.path.value
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
|
||||
- note: variable_in_nested_scope
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.utils
|
||||
default_config = {"retries": 3, "timeout": 30}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
outer_var := "outer"
|
||||
some x in [1, 2]
|
||||
inner_var := "inner"
|
||||
config := data.test.utils.default_config
|
||||
result := {
|
||||
"outer": outer_var,
|
||||
"inner": inner_var,
|
||||
"x": x,
|
||||
"retries": config.retries
|
||||
}
|
||||
x == 2
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"outer": "outer", "inner": "inner", "x": 2, "retries": 3}
|
||||
|
||||
- note: computed_field_name_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.api
|
||||
endpoints = {
|
||||
"v1_users": "/api/v1/users",
|
||||
"v1_posts": "/api/v1/posts",
|
||||
"v2_users": "/api/v2/users"
|
||||
}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
version := "v1"
|
||||
resource := "users"
|
||||
key := sprintf("%s_%s", [version, resource])
|
||||
result := data.test.api.endpoints[key]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "/api/v1/users"
|
||||
50
tests/rvm/rego/cases/comparisons.yaml
Normal file
50
tests/rvm/rego/cases/comparisons.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Comparison Operations Test Suite
|
||||
# Tests comparison operators: ==, <, >, <=, >=, !=
|
||||
|
||||
cases:
|
||||
- note: comparison_equals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := (5 == 5)
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: comparison_not_equals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := (5 == 3)
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: false
|
||||
|
||||
- note: comparison_less_than
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := (3 < 5)
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: comparison_greater_than
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := (7 > 5)
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
15
tests/rvm/rego/cases/comprehensions.yaml
Normal file
15
tests/rvm/rego/cases/comprehensions.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Arithmetic Operations Test Suite
|
||||
# Tests basic arithmetic operations: addition, multiplication, division, subtraction
|
||||
|
||||
cases:
|
||||
- note: comprehension_basic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [(x * 2) | some x in [1, 2, 3]]
|
||||
query: data.test.main
|
||||
want_result: [2, 4, 6]
|
||||
232
tests/rvm/rego/cases/default_rules.yaml
Normal file
232
tests/rvm/rego/cases/default_rules.yaml
Normal file
@@ -0,0 +1,232 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Default Rules Test Suite
|
||||
# Tests default rule evaluation when complete rules have no successful definitions
|
||||
|
||||
cases:
|
||||
- note: default_rule_basic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default allow := false
|
||||
allow := true if {
|
||||
false # This will always fail
|
||||
}
|
||||
query: data.test.allow
|
||||
want_result: false
|
||||
|
||||
- note: default_rule_with_multiple_definitions_all_fail
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default result := "default_value"
|
||||
result := "success1" if {
|
||||
false # This will fail
|
||||
}
|
||||
result := "success2" if {
|
||||
input.nonexistent == "value" # This will fail
|
||||
}
|
||||
result := "success3" if {
|
||||
1 == 2 # This will fail
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: "default_value"
|
||||
|
||||
- note: default_rule_not_used_when_definition_succeeds
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default allow := false
|
||||
allow := true if {
|
||||
1 == 1 # This will succeed
|
||||
}
|
||||
query: data.test.allow
|
||||
want_result: true
|
||||
|
||||
- note: default_rule_with_object_key
|
||||
skip: true # TODO: Fix rule type classification for config["timeout"] - should be Complete, not PartialObject
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default config["timeout"] := 30
|
||||
config["timeout"] := 60 if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.config.timeout
|
||||
want_result: 30
|
||||
|
||||
- note: default_rule_complex_value
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default settings := {
|
||||
"enabled": false,
|
||||
"retries": 3,
|
||||
"timeout": 30
|
||||
}
|
||||
settings := {
|
||||
"enabled": true,
|
||||
"retries": 5,
|
||||
"timeout": 60
|
||||
} if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.settings
|
||||
want_result:
|
||||
enabled: false
|
||||
retries: 3
|
||||
timeout: 30
|
||||
|
||||
- note: default_rule_with_array
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default items := ["default1", "default2"]
|
||||
items := ["actual1", "actual2"] if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.items
|
||||
want_result: ["default1", "default2"]
|
||||
|
||||
- note: default_rule_with_input_dependency
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default result := "no_user"
|
||||
result := "admin" if {
|
||||
input.user.role == "admin"
|
||||
}
|
||||
result := "user" if {
|
||||
input.user.role == "user"
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: "no_user"
|
||||
|
||||
- note: default_rule_with_input_dependency_success
|
||||
data: {}
|
||||
input:
|
||||
user:
|
||||
role: "admin"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default result := "no_user"
|
||||
result := "admin" if {
|
||||
input.user.role == "admin"
|
||||
}
|
||||
result := "user" if {
|
||||
input.user.role == "user"
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: "admin"
|
||||
|
||||
- note: default_rule_with_data_dependency
|
||||
data:
|
||||
config:
|
||||
mode: "production"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default debug_mode := false
|
||||
debug_mode := true if {
|
||||
data.config.mode == "development"
|
||||
}
|
||||
query: data.test.debug_mode
|
||||
want_result: false
|
||||
|
||||
- note: default_rule_nested_package
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test.auth
|
||||
default allow := false
|
||||
allow := true if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.auth.allow
|
||||
want_result: false
|
||||
|
||||
- note: multiple_default_rules_different_names
|
||||
skip: true
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default allow := false
|
||||
default deny := true
|
||||
allow := true if {
|
||||
false # This will fail
|
||||
}
|
||||
deny := false if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test
|
||||
want_result:
|
||||
allow: false
|
||||
deny: true
|
||||
|
||||
- note: default_rule_with_computed_value
|
||||
skip: true
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
base_timeout := 10
|
||||
default timeout := base_timeout * 3
|
||||
timeout := base_timeout * 6 if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.timeout
|
||||
want_result: 30
|
||||
|
||||
- note: default_rule_undefined_vs_default
|
||||
skip: true
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default has_default := "default"
|
||||
# no_default rule has no default and no successful definitions
|
||||
no_default := "success" if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test
|
||||
want_result:
|
||||
has_default: "default"
|
||||
|
||||
- note: default_rule_with_function_call
|
||||
skip: true
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
helper_func := "helper_result"
|
||||
default result := helper_func
|
||||
result := "success" if {
|
||||
false # This will fail
|
||||
}
|
||||
query: data.test.result
|
||||
want_result: "helper_result"
|
||||
|
||||
- note: default_rule_consistency_check
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
default value := 42
|
||||
value := 42 if {
|
||||
true # This succeeds with same value as default
|
||||
}
|
||||
value := 99 if {
|
||||
false # This fails
|
||||
}
|
||||
query: data.test.value
|
||||
want_result: 42
|
||||
260
tests/rvm/rego/cases/destructuring.yaml
Normal file
260
tests/rvm/rego/cases/destructuring.yaml
Normal file
@@ -0,0 +1,260 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Destructuring Pattern Test Suite
|
||||
# Tests destructuring patterns in assignments, function parameters, and some-in loops
|
||||
# Note: Set destructuring is not supported by Rego and should produce compilation errors
|
||||
|
||||
cases:
|
||||
# Basic array destructuring with colon assignment
|
||||
- note: array_destructuring_basic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [a, b] if {
|
||||
[a, b] := [1, 2]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [1, 2]
|
||||
|
||||
# Array destructuring with equals assignment
|
||||
- note: array_destructuring_equals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [x, y, z] if {
|
||||
arr := [10, 20, 30]
|
||||
[x, y, z] = arr
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [10, 20, 30]
|
||||
|
||||
# Object destructuring with colon assignment
|
||||
- note: object_destructuring_basic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [name, age] if {
|
||||
{"name": name, "age": age} := {"name": "Alice", "age": 30}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: ["Alice", 30]
|
||||
|
||||
# Object destructuring with equals assignment
|
||||
- note: object_destructuring_equals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [x, y] if {
|
||||
obj := {"x": 100, "y": 200}
|
||||
{"x": x, "y": y} = obj
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [100, 200]
|
||||
|
||||
# Nested array destructuring
|
||||
- note: nested_array_destructuring
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [a, c, d] if {
|
||||
[[a, b], [c, d]] := [[1, 2], [3, 4]]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: [1, 3, 4]
|
||||
|
||||
# Array destructuring in function parameters
|
||||
- note: array_destructuring_function_param
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
add_first_two([x, y]) := x + y
|
||||
main := add_first_two([5, 7])
|
||||
query: data.test.main
|
||||
want_result: 12
|
||||
|
||||
# Object destructuring in function parameters
|
||||
- note: object_destructuring_function_param
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
get_name({"name": name}) := name
|
||||
main := get_name({"name": "Bob", "age": 25})
|
||||
query: data.test.main
|
||||
want_result: "Bob"
|
||||
|
||||
# Mixed array and object destructuring
|
||||
- note: mixed_destructuring
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [name, x, y] if {
|
||||
[user, {"x": x, "y": y}] := [{"name": "Grace"}, {"x": 1, "y": 2}]
|
||||
{"name": name} = user
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: ["Grace", 1, 2]
|
||||
|
||||
# Destructuring with literal matching
|
||||
- note: destructuring_with_literals
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := value if {
|
||||
[1, value, 3] := [1, 42, 3]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 42
|
||||
|
||||
# SET DESTRUCTURING ERROR CASES - These should fail compilation
|
||||
# RVM correctly rejects these, but interpreter incorrectly allows them
|
||||
|
||||
# Set destructuring in colon assignment should error
|
||||
- note: set_destructuring_colon_error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
{a, b} := {1, 2, 3}
|
||||
result := [a, b]
|
||||
}
|
||||
query: data.test.main
|
||||
want_error: "assignment operator := requires left-hand side to have bindable variables"
|
||||
allow_interpreter_success: true
|
||||
|
||||
# Set destructuring in function parameters should error
|
||||
- note: set_destructuring_function_param_error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
has_element({x, y}, elem) := elem in {x, y}
|
||||
main := has_element({10, 20}, 20)
|
||||
query: data.test.main
|
||||
want_error: "Undefined variable"
|
||||
allow_interpreter_success: true
|
||||
|
||||
# Set destructuring in equals assignment should error
|
||||
- note: set_destructuring_equals_error
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
s := {1, 2}
|
||||
{x, y} = s
|
||||
result := [x, y]
|
||||
}
|
||||
query: data.test.main
|
||||
want_error: "Undefined variable"
|
||||
allow_interpreter_success: true
|
||||
|
||||
# Option 2: Function parameter destructuring with multiple definitions and definition-level failure
|
||||
- note: function_param_destructuring_multiple_definitions
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
# This function has multiple definitions with different parameter patterns
|
||||
# Only the matching definition should succeed, others should fail at definition level
|
||||
process_input([x, y]) := sprintf("array: %v, %v", [x, y])
|
||||
process_input([x, y, z]) := sprintf("array: %v, %v, %v", [x, y, z])
|
||||
process_input({"name": name, "age": age}) := sprintf("object: %s is %d", [name, age])
|
||||
|
||||
# Test with 2-element array - should match first definition
|
||||
test_2_elements := process_input([1, 2])
|
||||
|
||||
# Test with 3-element array - should match second definition
|
||||
test_3_elements := process_input([1, 2, 3])
|
||||
|
||||
# Test with object - should match third definition
|
||||
test_object := process_input({"name": "Alice", "age": 30})
|
||||
|
||||
# Combined result for testing
|
||||
main := {
|
||||
"test_2_elements": test_2_elements,
|
||||
"test_3_elements": test_3_elements,
|
||||
"test_object": test_object
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
test_2_elements: "array: 1, 2"
|
||||
test_3_elements: "array: 1, 2, 3"
|
||||
test_object: "object: Alice is 30"
|
||||
|
||||
|
||||
# Option 2: Complex nested destructuring in function parameters
|
||||
- note: function_param_nested_destructuring
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
# Function with nested destructuring patterns
|
||||
extract_info({"user": {"name": name, "details": {"age": age, "city": city}}, "active": active}) := {
|
||||
"user_name": name,
|
||||
"user_age": age,
|
||||
"user_city": city,
|
||||
"is_active": active
|
||||
}
|
||||
|
||||
main := extract_info({
|
||||
"user": {
|
||||
"name": "Bob",
|
||||
"details": {
|
||||
"age": 25,
|
||||
"city": "Seattle"
|
||||
}
|
||||
},
|
||||
"active": true
|
||||
})
|
||||
query: data.test.main
|
||||
want_result:
|
||||
user_name: "Bob"
|
||||
user_age: 25
|
||||
user_city: "Seattle"
|
||||
is_active: true
|
||||
|
||||
# Option 2: Mixed destructuring and non-destructuring definitions
|
||||
- note: function_mixed_destructuring_and_simple
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
# Function with mixed parameter styles - some with destructuring, some without
|
||||
handle_request(method) := sprintf("simple method: %s", [method]) if {
|
||||
method in ["GET", "POST", "PUT", "DELETE"]
|
||||
}
|
||||
handle_request({"method": method, "path": path}) := sprintf("structured request: %s %s", [method, path])
|
||||
handle_request({"method": method, "headers": {"auth": token}}) := sprintf("authenticated %s with token %s", [method, token])
|
||||
|
||||
# Test simple string parameter - should match first definition
|
||||
test_simple := handle_request("GET")
|
||||
|
||||
# Test structured request - should match second definition
|
||||
test_structured := handle_request({"method": "POST", "path": "/users"})
|
||||
|
||||
# Test with auth header - should match third definition
|
||||
test_auth := handle_request({"method": "PUT", "headers": {"auth": "abc123"}})
|
||||
|
||||
# Test that fails all patterns - this should be undefined
|
||||
test_invalid := handle_request(42)
|
||||
|
||||
# Combined result for testing
|
||||
main := {
|
||||
"test_simple": test_simple,
|
||||
"test_structured": test_structured,
|
||||
"test_auth": test_auth,
|
||||
"test_invalid": test_invalid
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
72
tests/rvm/rego/cases/examples.yaml
Normal file
72
tests/rvm/rego/cases/examples.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
cases:
|
||||
- note: server_security_policy
|
||||
data: {}
|
||||
input:
|
||||
servers:
|
||||
- id: "app"
|
||||
protocols: ["https", "ssh"]
|
||||
ports: ["p1", "p2", "p3"]
|
||||
- id: "db"
|
||||
protocols: ["mysql"]
|
||||
ports: ["p3"]
|
||||
- id: "cache"
|
||||
protocols: ["memcache"]
|
||||
ports: ["p3"]
|
||||
- id: "ci"
|
||||
protocols: ["http"]
|
||||
ports: ["p1", "p2"]
|
||||
- id: "busybox"
|
||||
protocols: ["telnet"]
|
||||
ports: ["p1"]
|
||||
networks:
|
||||
- id: "net1"
|
||||
public: false
|
||||
- id: "net2"
|
||||
public: false
|
||||
- id: "net3"
|
||||
public: true
|
||||
- id: "net4"
|
||||
public: true
|
||||
ports:
|
||||
- id: "p1"
|
||||
network: "net1"
|
||||
- id: "p2"
|
||||
network: "net3"
|
||||
- id: "p3"
|
||||
network: "net2"
|
||||
modules:
|
||||
- |
|
||||
package example
|
||||
|
||||
default allow := false # unless otherwise defined, allow is false
|
||||
|
||||
allow := r if { # allow is true if...
|
||||
r := {
|
||||
"outcome": count(violation) == 0, # there are zero violations.
|
||||
"violations": violation # the violations are listed in the output.
|
||||
}
|
||||
}
|
||||
|
||||
violation contains server.id if { # a server is in the violation set if...
|
||||
server := input.servers[_] # it exists in the input.servers collection and...
|
||||
server.protocols[_] == "telnet" # it contains the "telnet" protocol.
|
||||
}
|
||||
|
||||
violation contains server.id if { # a server is in the violation set if...
|
||||
some server
|
||||
public_server[server] # it exists in the 'public_server' set and...
|
||||
server.protocols[_] == "http" # it contains the insecure "http" protocol.
|
||||
}
|
||||
|
||||
public_server contains server if { # a server exists in the public_server set if...
|
||||
some i, j
|
||||
server := input.servers[_] # it exists in the input.servers collection and...
|
||||
server.ports[_] == input.ports[i].id # it references a port in the input.ports collection and...
|
||||
input.ports[i].network == input.networks[j].id # the port references a network in the input.networks collection and...
|
||||
input.networks[j].public # the network is public.
|
||||
}
|
||||
query: data.example.allow
|
||||
want_result:
|
||||
outcome: false
|
||||
violations:
|
||||
set!: ["ci", "busybox"]
|
||||
230
tests/rvm/rego/cases/function_rules.yaml
Normal file
230
tests/rvm/rego/cases/function_rules.yaml
Normal file
@@ -0,0 +1,230 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Function Rules Test Suite
|
||||
# Tests user-defined function rule calls with arguments
|
||||
# Covers function definitions, argument passing, return values, and consistency
|
||||
|
||||
cases:
|
||||
- note: simple_function_call
|
||||
description: Test basic function rule definition and call
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Define a simple function rule
|
||||
add_ten(x) := x + 10
|
||||
|
||||
# Call the function
|
||||
main := add_ten(5)
|
||||
query: data.test.main
|
||||
want_result: 15
|
||||
|
||||
- note: function_with_multiple_args
|
||||
description: Test function rule with multiple arguments
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Function that adds two numbers
|
||||
add(x, y) := x + y
|
||||
|
||||
# Call with two arguments
|
||||
main := add(7, 3)
|
||||
query: data.test.main
|
||||
want_result: 10
|
||||
|
||||
- note: function_with_variable_args
|
||||
description: Test function call with variables as arguments
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
multiply(x, y) := x * y
|
||||
|
||||
main := result if {
|
||||
a := 4
|
||||
b := 6
|
||||
result := multiply(a, b)
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 24
|
||||
|
||||
- note: function_returning_object
|
||||
description: Test function that returns an object
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
make_person(name, age) := {"name": name, "age": age}
|
||||
|
||||
main := make_person("Alice", 30)
|
||||
query: data.test.main
|
||||
want_result: {"name": "Alice", "age": 30}
|
||||
|
||||
- note: function_returning_array
|
||||
description: Test function that returns an array
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
make_range(start, end) := [start, end] if start <= end
|
||||
|
||||
main := make_range(1, 3)
|
||||
query: data.test.main
|
||||
want_result: [1, 3]
|
||||
|
||||
- note: nested_function_calls
|
||||
description: Test nested function calls
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
double(x) := x * 2
|
||||
add_one(x) := x + 1
|
||||
|
||||
main := double(add_one(5))
|
||||
query: data.test.main
|
||||
want_result: 12
|
||||
|
||||
- note: function_with_condition
|
||||
description: Test function rule with conditional body
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
max(x, y) := x if x >= y
|
||||
max(x, y) := y if y > x
|
||||
|
||||
main := max(7, 3)
|
||||
query: data.test.main
|
||||
want_result: 7
|
||||
|
||||
- note: function_consistency_check
|
||||
description: Test that function definitions must be consistent
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# These definitions would be inconsistent if both conditions were true
|
||||
inconsistent_func(x) := x + 1 if x < 5
|
||||
inconsistent_func(x) := x + 2 if x < 5
|
||||
|
||||
# This should work for x >= 5
|
||||
main := inconsistent_func(10)
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
|
||||
- note: function_with_undefined_result
|
||||
description: Test function that can return undefined
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Function only defined for positive numbers
|
||||
positive_double(x) := x * 2 if x > 0
|
||||
|
||||
# Calling with negative number should return undefined
|
||||
main := positive_double(-1)
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
|
||||
- note: function_using_data
|
||||
description: Test function that accesses global data
|
||||
data: {"multiplier": 3}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
scale(x) := x * data.multiplier
|
||||
|
||||
main := scale(5)
|
||||
query: data.test.main
|
||||
want_result: 15
|
||||
|
||||
- note: function_using_input
|
||||
description: Test function that accesses input
|
||||
data: {}
|
||||
input: {"base": 10}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
add_to_base(x) := x + input.base
|
||||
|
||||
main := add_to_base(5)
|
||||
query: data.test.main
|
||||
want_result: 15
|
||||
|
||||
- note: function_with_complex_logic
|
||||
description: Test function with complex conditional logic
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
classify_number(x) := "negative" if x < 0
|
||||
classify_number(x) := "zero" if x == 0
|
||||
classify_number(x) := "small positive" if {
|
||||
x > 0
|
||||
x <= 10
|
||||
}
|
||||
classify_number(x) := "large positive" if x > 10
|
||||
|
||||
main := classify_number(5)
|
||||
query: data.test.main
|
||||
want_result: "small positive"
|
||||
|
||||
- note: function_with_array_processing
|
||||
description: Test function that processes arrays
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
first_element(arr) := arr[0]
|
||||
|
||||
main := first_element([1, 2, 3])
|
||||
query: data.test.main
|
||||
want_result: 1
|
||||
|
||||
- note: function_with_object_processing
|
||||
description: Test function that processes objects
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
get_field(obj, field) := obj[field]
|
||||
|
||||
main := get_field({"name": "Bob", "age": 25}, "name")
|
||||
query: data.test.main
|
||||
want_result: "Bob"
|
||||
|
||||
- note: function_call_chain
|
||||
description: Test chain of function calls
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
step1(x) := x + 1
|
||||
step2(x) := x * 2
|
||||
step3(x) := x - 3
|
||||
|
||||
main := result if {
|
||||
a := step1(5) # 6
|
||||
b := step2(a) # 12
|
||||
result := step3(b) # 9
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 9
|
||||
322
tests/rvm/rego/cases/local_chained_access.yaml
Normal file
322
tests/rvm/rego/cases/local_chained_access.yaml
Normal file
@@ -0,0 +1,322 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Non-Data Prefix Chained Access Test Suite
|
||||
# Tests chained reference expressions without data prefix (local rules and variables)
|
||||
|
||||
cases:
|
||||
- note: direct_rule_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
user_config = {"name": "Alice", "role": "admin", "active": true}
|
||||
main := result if {
|
||||
result := user_config.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice"
|
||||
|
||||
- note: chained_rule_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
settings = {
|
||||
"database": {"host": "localhost", "port": 5432},
|
||||
"cache": {"enabled": true, "ttl": 300}
|
||||
}
|
||||
main := result if {
|
||||
result := settings.database.host
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "localhost"
|
||||
|
||||
- note: local_variable_with_fields
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
config := {"server": {"name": "web1", "port": 8080}}
|
||||
result := config.server.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "web1"
|
||||
|
||||
- note: rule_access_with_dynamic_index
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
servers = {
|
||||
"web": {"status": "running", "cpu": 45},
|
||||
"db": {"status": "stopped", "cpu": 0}
|
||||
}
|
||||
main := result if {
|
||||
server_type := "web"
|
||||
result := servers[server_type].status
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "running"
|
||||
|
||||
- note: mixed_static_dynamic_local_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
metrics = {
|
||||
"hourly": [
|
||||
{"timestamp": "2023-01-01T10:00:00Z", "value": 100},
|
||||
{"timestamp": "2023-01-01T11:00:00Z", "value": 150}
|
||||
]
|
||||
}
|
||||
main := result if {
|
||||
period := "hourly"
|
||||
index := 1
|
||||
result := metrics[period][index].value
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 150
|
||||
|
||||
- note: nested_rule_calls_without_data_prefix
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
base_config = {"timeout": 30, "retries": 3}
|
||||
extended_config = {"base": base_config, "debug": true}
|
||||
main := result if {
|
||||
result := extended_config.base.timeout
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 30
|
||||
|
||||
- note: local_var_precedence_over_same_package_rule
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
config = {"source": "rule"}
|
||||
main := result if {
|
||||
config := {"source": "local"}
|
||||
result := config.source
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "local"
|
||||
|
||||
- note: array_access_without_data_prefix
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
items = [
|
||||
{"id": 1, "name": "first"},
|
||||
{"id": 2, "name": "second"},
|
||||
{"id": 3, "name": "third"}
|
||||
]
|
||||
main := result if {
|
||||
idx := 2
|
||||
result := items[idx].name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "third"
|
||||
|
||||
- note: string_literal_bracket_local_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
status_codes = {
|
||||
"200": "OK",
|
||||
"404": "Not Found",
|
||||
"500": "Internal Server Error"
|
||||
}
|
||||
main := result if {
|
||||
result := status_codes["404"]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Not Found"
|
||||
|
||||
- note: complex_local_chaining
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
app_config = {
|
||||
"environments": {
|
||||
"dev": {
|
||||
"database": {"url": "dev.db.com", "pool_size": 5},
|
||||
"logging": {"level": "debug"}
|
||||
},
|
||||
"prod": {
|
||||
"database": {"url": "prod.db.com", "pool_size": 20},
|
||||
"logging": {"level": "error"}
|
||||
}
|
||||
}
|
||||
}
|
||||
main := result if {
|
||||
env := "prod"
|
||||
result := app_config.environments[env].database.url
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "prod.db.com"
|
||||
|
||||
- note: rule_with_computed_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
api_versions = {
|
||||
"v1": {"path": "/api/v1", "deprecated": true},
|
||||
"v2": {"path": "/api/v2", "deprecated": false}
|
||||
}
|
||||
main := result if {
|
||||
version := "v2"
|
||||
field := "deprecated"
|
||||
result := api_versions[version][field]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: false
|
||||
|
||||
- note: nested_local_variables_with_chaining
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
user := {"profile": {"settings": {"theme": "dark", "notifications": true}}}
|
||||
theme_setting := user.profile.settings.theme
|
||||
result := theme_setting
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "dark"
|
||||
|
||||
- note: rule_reference_with_multiple_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
network_config = {
|
||||
"interfaces": {
|
||||
"eth0": {"ip": "192.168.1.10", "mask": "255.255.255.0"},
|
||||
"eth1": {"ip": "10.0.0.5", "mask": "255.255.0.0"}
|
||||
}
|
||||
}
|
||||
main := result if {
|
||||
interface := "eth0"
|
||||
result := {
|
||||
"ip": network_config.interfaces[interface].ip,
|
||||
"mask": network_config.interfaces[interface].mask
|
||||
}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"ip": "192.168.1.10", "mask": "255.255.255.0"}
|
||||
|
||||
- note: undefined_rule_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := nonexistent_rule.field
|
||||
}
|
||||
query: data.test.main
|
||||
want_error: "undefined variable"
|
||||
|
||||
- note: undefined_field_on_existing_rule
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
my_rule = {"existing": "value"}
|
||||
main := result if {
|
||||
result := my_rule.nonexistent_field
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
|
||||
- note: variable_assignment_with_chained_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
source_data = {
|
||||
"users": {
|
||||
"alice": {"email": "alice@example.com", "active": true},
|
||||
"bob": {"email": "bob@example.com", "active": false}
|
||||
}
|
||||
}
|
||||
main := result if {
|
||||
user_id := "alice"
|
||||
user_email := source_data.users[user_id].email
|
||||
result := user_email
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "alice@example.com"
|
||||
|
||||
- note: rule_call_in_middle_of_chain
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
get_user_data = {"profile": {"name": "Alice", "age": 30}}
|
||||
main := result if {
|
||||
result := get_user_data.profile.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice"
|
||||
|
||||
- note: local_var_shadowing_with_different_structure
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
config = {"type": "global", "value": 100}
|
||||
main := result if {
|
||||
config := [{"type": "local", "value": 200}]
|
||||
result := config[0].type
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "local"
|
||||
|
||||
- note: deep_nested_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
deep_structure = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": {
|
||||
"level4": {
|
||||
"level5": {"final_value": "found it!"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
main := result if {
|
||||
result := deep_structure.level1.level2.level3.level4.level5.final_value
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "found it!"
|
||||
|
||||
- note: bracket_access_with_computed_key
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
lookup_table = {
|
||||
"key_1": "value_1",
|
||||
"key_2": "value_2",
|
||||
"key_3": "value_3"
|
||||
}
|
||||
main := result if {
|
||||
prefix := "key"
|
||||
suffix := 2
|
||||
key := sprintf("%s_%d", [prefix, suffix])
|
||||
result := lookup_table[key]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "value_2"
|
||||
89
tests/rvm/rego/cases/loops_and_quantifiers.yaml
Normal file
89
tests/rvm/rego/cases/loops_and_quantifiers.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Loops and Quantifiers Test Suite
|
||||
# Tests basic loop constructs, quantifiers (some/every), and comprehensions
|
||||
|
||||
cases:
|
||||
- note: basic_variable_test
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
x := 5
|
||||
result := x > 2
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: basic_some_test
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
some x in [1, 2, 3]
|
||||
x > 2
|
||||
result := x
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 3
|
||||
|
||||
- note: basic_every_test
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
every x in [1, 2, 3] {
|
||||
x > 0
|
||||
}
|
||||
result := true
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: simple_loop_test
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
x := 1
|
||||
y := 2
|
||||
result := x * y
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 2
|
||||
|
||||
- note: loop_array_comprehension
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := [(x * 2) | x := [1, 2, 3][_]]
|
||||
query: data.test.main
|
||||
want_result: [2, 4, 6]
|
||||
|
||||
- note: loop_set_comprehension
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := {(x * 2) | x := [1, 2, 3][_]}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!: [2, 4, 6]
|
||||
|
||||
- note: loop_object_comprehension
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := {k: (v * 2) | v := {"a": 1, "b": 2, "c": 3}[k]}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
a: 2
|
||||
b: 4
|
||||
c: 6
|
||||
78
tests/rvm/rego/cases/multiple_entry_points.yaml
Normal file
78
tests/rvm/rego/cases/multiple_entry_points.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
cases:
|
||||
- note: "multiple entry points - basic allow and deny rules"
|
||||
modules:
|
||||
- |
|
||||
package example
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
input.method == "GET"
|
||||
}
|
||||
|
||||
allow if {
|
||||
input.user == "admin"
|
||||
}
|
||||
|
||||
default deny := true
|
||||
|
||||
deny if {
|
||||
input.method == "DELETE"
|
||||
}
|
||||
query: "data.example.allow"
|
||||
entry_points:
|
||||
- "data.example.allow"
|
||||
- "data.example.deny"
|
||||
input: {"method": "GET", "user": "guest"}
|
||||
want_result: true
|
||||
|
||||
- note: "multiple entry points - computed rules with want_results"
|
||||
modules:
|
||||
- |
|
||||
package math
|
||||
|
||||
result := 42
|
||||
|
||||
doubled := 84
|
||||
|
||||
status := "computed"
|
||||
query: "data.math.result"
|
||||
entry_points:
|
||||
- "data.math.result"
|
||||
- "data.math.doubled"
|
||||
- "data.math.status"
|
||||
want_results:
|
||||
- 42
|
||||
- 84
|
||||
- "computed"
|
||||
|
||||
- note: "multiple entry points - different packages with want_results"
|
||||
modules:
|
||||
- |
|
||||
package auth
|
||||
|
||||
default authenticated := false
|
||||
|
||||
authenticated if {
|
||||
input.token == "valid"
|
||||
}
|
||||
- |
|
||||
package authz
|
||||
|
||||
default authorized := false
|
||||
|
||||
authorized if {
|
||||
input.user == "admin"
|
||||
}
|
||||
|
||||
authorized if {
|
||||
input.role == "manager"
|
||||
}
|
||||
query: "data.auth.authenticated"
|
||||
entry_points:
|
||||
- "data.auth.authenticated"
|
||||
- "data.authz.authorized"
|
||||
input: {"token": "valid", "user": "guest"}
|
||||
want_results:
|
||||
- true
|
||||
- false
|
||||
108
tests/rvm/rego/cases/objects.yaml
Normal file
108
tests/rvm/rego/cases/objects.yaml
Normal file
@@ -0,0 +1,108 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Objects Test Suite
|
||||
# Tests object creation, nested objects, field access, and dynamic field operations
|
||||
|
||||
cases:
|
||||
- note: object_creation
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := {"name": "Alice", "age": 30, "active": true}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"name": "Alice", "age": 30, "active": true}
|
||||
|
||||
- note: nested_objects
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := {
|
||||
"user": {
|
||||
"name": "Alice",
|
||||
"details": {"age": 30, "active": true}
|
||||
},
|
||||
"config": {"debug": false}
|
||||
}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"user": {"name": "Alice", "details": {"age": 30, "active": true}}, "config": {"debug": false}}
|
||||
|
||||
- note: object_field_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
user := {"name": "Alice", "age": 30}
|
||||
main := result if {
|
||||
result := user.name
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice"
|
||||
|
||||
- note: nested_object_access
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
user := {
|
||||
"name": "Alice",
|
||||
"details": {
|
||||
"age": 30,
|
||||
"profile": {
|
||||
"country": "USA",
|
||||
"city": "Seattle"
|
||||
}
|
||||
}
|
||||
}
|
||||
main := result if {
|
||||
result := user.details.profile.city
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Seattle"
|
||||
|
||||
- note: dynamic_field_name_get
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
user := {"name": "Alice", "age": 30, "status": "active"}
|
||||
field_name := "status"
|
||||
main := result if {
|
||||
result := user[field_name]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "active"
|
||||
|
||||
- note: dynamic_field_name_set
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
field_name := "email"
|
||||
field_value := "alice@example.com"
|
||||
main := result if {
|
||||
result := {field_name: field_value, "name": "Alice"}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"email": "alice@example.com", "name": "Alice"}
|
||||
|
||||
- note: dynamic_object_construction
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
name_field := "username"
|
||||
name_value := "alice123"
|
||||
age_field := "user_age"
|
||||
age_value := 25
|
||||
main := result if {
|
||||
result := {name_field: name_value, age_field: age_value}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"username": "alice123", "user_age": 25}
|
||||
145
tests/rvm/rego/cases/rule_data_conflicts.yaml
Normal file
145
tests/rvm/rego/cases/rule_data_conflicts.yaml
Normal file
@@ -0,0 +1,145 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Rule-Data Conflict Detection Test Suite
|
||||
# Tests that the RVM properly detects conflicts between rule definitions and data documents
|
||||
|
||||
cases:
|
||||
- note: no_conflict_different_packages
|
||||
data:
|
||||
users:
|
||||
alice:
|
||||
role: "guest"
|
||||
level: 1
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
result := data.users.alice.role
|
||||
query: data.test.result
|
||||
want_result: "guest"
|
||||
|
||||
- note: no_conflict_different_paths
|
||||
data:
|
||||
config:
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
users := {
|
||||
"alice": {
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
query: data.test.users
|
||||
want_result:
|
||||
alice:
|
||||
role: "admin"
|
||||
|
||||
- note: conflict_same_path_rule_vs_data
|
||||
data:
|
||||
test:
|
||||
users:
|
||||
alice:
|
||||
role: "guest"
|
||||
modules:
|
||||
- |
|
||||
package test.users
|
||||
alice := {
|
||||
"role": "admin",
|
||||
"level": 5
|
||||
}
|
||||
query: data.test.users.alice
|
||||
want_error: "Conflict: rule defines path 'test.users.alice' but data also provides this path"
|
||||
# RVM detects this conflict, but interpreter may not - that's acceptable
|
||||
allow_interpreter_success: true
|
||||
|
||||
- note: conflict_rule_parent_data_child
|
||||
data:
|
||||
test:
|
||||
config:
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
config := {
|
||||
"app_name": "myapp",
|
||||
"version": "1.0"
|
||||
}
|
||||
query: data.test.config
|
||||
want_error: "Conflict: rule defines path 'test.config' but data also provides this path"
|
||||
# RVM detects this conflict, but interpreter may not - that's acceptable
|
||||
allow_interpreter_success: true
|
||||
|
||||
- note: conflict_data_parent_rule_child
|
||||
data:
|
||||
test:
|
||||
users: "not an object"
|
||||
modules:
|
||||
- |
|
||||
package test.users
|
||||
alice := {"role": "admin"}
|
||||
query: data.test.users.alice
|
||||
want_error: "Conflict: rule defines subpaths under 'test.users' but data provides a non-object value at this path"
|
||||
# RVM detects this conflict, but interpreter may not - that's acceptable
|
||||
allow_interpreter_success: true
|
||||
|
||||
- note: no_conflict_nested_coexistence
|
||||
data:
|
||||
static_config:
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user_data:
|
||||
preferences:
|
||||
theme: "dark"
|
||||
modules:
|
||||
- |
|
||||
package dynamic
|
||||
users := {
|
||||
"alice": {
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
computed_stats := {
|
||||
"total_users": 42
|
||||
}
|
||||
query: data.dynamic.users
|
||||
want_result:
|
||||
alice:
|
||||
role: "admin"
|
||||
|
||||
- note: no_conflict_multiple_rule_levels
|
||||
data:
|
||||
test:
|
||||
api:
|
||||
v1:
|
||||
endpoints: ["users", "posts"]
|
||||
modules:
|
||||
- |
|
||||
package test.api.v1
|
||||
auth := {
|
||||
"required": true,
|
||||
"methods": ["jwt", "oauth"]
|
||||
}
|
||||
query: data.test.api.v1.auth
|
||||
want_result:
|
||||
required: true
|
||||
methods: ["jwt", "oauth"]
|
||||
|
||||
- note: no_conflict_rule_extends_data_object
|
||||
data:
|
||||
test:
|
||||
config:
|
||||
database:
|
||||
host: "localhost"
|
||||
modules:
|
||||
- |
|
||||
package test.config
|
||||
app_name := "myapp"
|
||||
version := "1.0"
|
||||
query: data.test.config.app_name
|
||||
want_result: "myapp"
|
||||
153
tests/rvm/rego/cases/set_rules.yaml
Normal file
153
tests/rvm/rego/cases/set_rules.yaml
Normal file
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Examples Test Suite
|
||||
# Tests real-world patterns and advanced Rego constructs
|
||||
|
||||
cases:
|
||||
- note: set_rules_with_contains
|
||||
data: {}
|
||||
input:
|
||||
user:
|
||||
role: "editor"
|
||||
name: "alice"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Define a set of allowed actions
|
||||
allowed_actions contains "read" if {
|
||||
input.user.role in ["viewer", "editor", "admin"]
|
||||
}
|
||||
|
||||
allowed_actions contains "write" if {
|
||||
input.user.role in ["editor", "admin"]
|
||||
}
|
||||
|
||||
allowed_actions contains "admin" if {
|
||||
input.user.role == "admin"
|
||||
}
|
||||
|
||||
# Check if a specific action is allowed
|
||||
allow_read := "read" in allowed_actions
|
||||
allow_write := "write" in allowed_actions
|
||||
allow_admin := "admin" in allowed_actions
|
||||
|
||||
# Main result combining all permissions
|
||||
main := {
|
||||
"allowed_actions": allowed_actions,
|
||||
"can_read": allow_read,
|
||||
"can_write": allow_write,
|
||||
"can_admin": allow_admin
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
allowed_actions:
|
||||
set!: ["read", "write"]
|
||||
can_read: true
|
||||
can_write: true
|
||||
can_admin: false
|
||||
|
||||
- note: set_membership_with_contains
|
||||
data: {}
|
||||
input:
|
||||
department: "engineering"
|
||||
role: "developer"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Define sets using contains
|
||||
valid_departments contains d if {
|
||||
some dept in ["engineering", "marketing", "sales"]
|
||||
d := dept
|
||||
}
|
||||
|
||||
sensitive_roles contains role if {
|
||||
some role in ["admin", "security", "finance"]
|
||||
r := role
|
||||
}
|
||||
|
||||
# Check membership
|
||||
is_valid_dept := input.department in valid_departments
|
||||
is_sensitive := input.role in sensitive_roles
|
||||
|
||||
# Access decision
|
||||
allow := is_valid_dept
|
||||
deny := is_sensitive
|
||||
|
||||
main := {
|
||||
"valid_departments": valid_departments,
|
||||
"sensitive_roles": sensitive_roles,
|
||||
"department_valid": is_valid_dept,
|
||||
"role_sensitive": is_sensitive,
|
||||
"allow": allow,
|
||||
"deny": deny
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
valid_departments:
|
||||
set!: ["engineering", "marketing", "sales"]
|
||||
sensitive_roles:
|
||||
set!: ["admin", "security", "finance"]
|
||||
department_valid: true
|
||||
role_sensitive: false
|
||||
allow: true
|
||||
deny: false
|
||||
|
||||
- note: conditional_set_contains
|
||||
data: {}
|
||||
input:
|
||||
user:
|
||||
active: true
|
||||
level: 3
|
||||
department: "engineering"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Conditional set rules
|
||||
permissions contains "read" if {
|
||||
input.user.active == true
|
||||
}
|
||||
|
||||
permissions contains "write" if {
|
||||
input.user.active == true
|
||||
input.user.level >= 2
|
||||
}
|
||||
|
||||
permissions contains "delete" if {
|
||||
input.user.active == true
|
||||
input.user.level >= 5
|
||||
input.user.department == "admin"
|
||||
}
|
||||
|
||||
main := permissions
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!: ["read", "write"]
|
||||
|
||||
- note: empty_set_contains
|
||||
data: {}
|
||||
input:
|
||||
user:
|
||||
role: "user"
|
||||
verified: false
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
# Set that might be empty based on conditions
|
||||
special_permissions contains "super_admin" if {
|
||||
input.user.role == "root"
|
||||
input.user.verified == true
|
||||
}
|
||||
|
||||
special_permissions contains "audit" if {
|
||||
input.user.role == "auditor"
|
||||
}
|
||||
|
||||
main := special_permissions
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!: []
|
||||
69
tests/rvm/rego/cases/sets.yaml
Normal file
69
tests/rvm/rego/cases/sets.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Sets Test Suite
|
||||
# Tests set creation, deduplication, membership testing, and nested sets
|
||||
|
||||
cases:
|
||||
- note: set_creation
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := {1, 2, 3, "hello", true}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!: [1, 2, 3, "hello", true]
|
||||
|
||||
- note: set_with_duplicates
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := {1, 2, 2, 3, 1}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!: [1, 2, 3]
|
||||
|
||||
- note: set_membership
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
my_set := {1, 2, 3, 4, 5}
|
||||
main := result if {
|
||||
result := 3 in my_set
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: true
|
||||
|
||||
- note: set_non_membership
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
my_set := {"a", "b", "c"}
|
||||
main := result if {
|
||||
result := "d" in my_set
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: false
|
||||
|
||||
- note: nested_sets
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := {{1, 2}, {3, 4}, {"a", "b"}}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
set!:
|
||||
- set!: [1, 2]
|
||||
- set!: [3, 4]
|
||||
- set!: ["a", "b"]
|
||||
50
tests/rvm/rego/cases/variables_and_rules.yaml
Normal file
50
tests/rvm/rego/cases/variables_and_rules.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Variables and Rules Test Suite
|
||||
# Tests variable assignment, rule definitions, and rule dependencies
|
||||
|
||||
cases:
|
||||
- note: variable_assignment
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
x := 42
|
||||
result := x
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 42
|
||||
|
||||
- note: rule_without_body
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main = 100
|
||||
query: data.test.main
|
||||
want_result: 100
|
||||
|
||||
- note: rule_dependency
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
x = 42
|
||||
main := result if {
|
||||
result := x + 10
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 52
|
||||
|
||||
- note: rule_undefined_condition_fails
|
||||
data: {}
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := "success" if {
|
||||
false # condition always fails
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
396
tests/rvm/rego/cases/virtual_data_document_lookup.yaml
Normal file
396
tests/rvm/rego/cases/virtual_data_document_lookup.yaml
Normal file
@@ -0,0 +1,396 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# VirtualDataDocumentLookup Test Suite
|
||||
# Tests the four cases of virtual data document lookup:
|
||||
# 1. All components consumed and rule index found -> evaluate rule
|
||||
# 2. Rule index found with remaining components -> evaluate rule then index result
|
||||
# 3. All components consumed but undefined -> apply components to data directly
|
||||
# 4. Subobject found -> panic (not yet implemented)
|
||||
|
||||
cases:
|
||||
# Case 1: All components consumed and rule index found
|
||||
- note: rule_index_all_components_consumed
|
||||
data:
|
||||
users:
|
||||
alice: {"name": "Alice", "age": 30}
|
||||
input:
|
||||
rule_name: "alice_profile"
|
||||
modules:
|
||||
- |
|
||||
package test.users
|
||||
alice_profile := data.users.alice
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.users[input.rule_name]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: {"name": "Alice", "age": 30}
|
||||
allow_interpreter_incorrect_behavior: true
|
||||
|
||||
# Case 2: Rule index found with remaining components
|
||||
- note: rule_index_with_remaining_components
|
||||
data:
|
||||
users:
|
||||
alice: {"name": "Alice", "age": 30, "profile": {"bio": "Software Engineer"}}
|
||||
input:
|
||||
rule_name: "alice_data"
|
||||
field1: "profile"
|
||||
field2: "bio"
|
||||
modules:
|
||||
- |
|
||||
package test.users
|
||||
alice_data := data.users.alice
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.users[input.rule_name][input.field1][input.field2]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Software Engineer"
|
||||
allow_interpreter_incorrect_behavior: true
|
||||
|
||||
# Case 2b: Rule index with multiple remaining components
|
||||
- note: rule_index_multiple_remaining_components
|
||||
data:
|
||||
config:
|
||||
app:
|
||||
settings: {"theme": "dark", "lang": "en"}
|
||||
input:
|
||||
rule: "app_config"
|
||||
path1: "settings"
|
||||
path2: "theme"
|
||||
modules:
|
||||
- |
|
||||
package test.config
|
||||
app_config := data.config.app
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.config[input.rule][input.path1][input.path2]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "dark"
|
||||
allow_interpreter_incorrect_behavior: true
|
||||
|
||||
# Case 3: All components consumed but no rule exists (direct data access)
|
||||
- note: direct_data_access_no_rules
|
||||
data:
|
||||
users:
|
||||
bob: {"name": "Bob", "age": 25}
|
||||
input:
|
||||
person: "bob"
|
||||
attribute: "name"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# No rule defined for data.users.bob, should access data directly
|
||||
result := data.users[input.person][input.attribute]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Bob"
|
||||
|
||||
# Case 3b: Direct nested data access (no rules)
|
||||
- note: direct_nested_data_access
|
||||
data:
|
||||
system:
|
||||
metrics:
|
||||
cpu: 85
|
||||
memory: 70
|
||||
input:
|
||||
metric: "cpu"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# No rules defined for data.system.metrics, access data directly
|
||||
result := data.system.metrics[input.metric]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 85
|
||||
|
||||
# Case 3c: Direct array indexing (no rules)
|
||||
- note: direct_array_indexing
|
||||
data:
|
||||
inventory:
|
||||
fruits: ["apple", "banana", "cherry"]
|
||||
input:
|
||||
collection: "fruits"
|
||||
index: 1
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# No rules defined for data.inventory.fruits, access array directly
|
||||
result := data.inventory[input.collection][input.index]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "banana"
|
||||
|
||||
# Mixed case: Rule exists at intermediate level, then data access
|
||||
- note: rule_at_intermediate_level
|
||||
data:
|
||||
company:
|
||||
employees:
|
||||
- {"name": "Alice", "dept": "Engineering"}
|
||||
- {"name": "Bob", "dept": "Marketing"}
|
||||
input:
|
||||
rule_name: "staff"
|
||||
idx: 0
|
||||
field: "name"
|
||||
modules:
|
||||
- |
|
||||
package test.company
|
||||
staff := data.company.employees
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# Rule exists at data.test.company.staff, then access array element
|
||||
result := data.test.company[input.rule_name][input.idx][input.field]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "Alice"
|
||||
allow_interpreter_incorrect_behavior: true
|
||||
|
||||
# Case with dynamic indexing via register
|
||||
- note: rule_with_dynamic_indexing
|
||||
data:
|
||||
products:
|
||||
electronics: {"laptop": 1200, "phone": 800}
|
||||
clothing: {"shirt": 25, "pants": 50}
|
||||
input:
|
||||
rule: "electronics_catalog"
|
||||
item: "laptop"
|
||||
modules:
|
||||
- |
|
||||
package test.products
|
||||
electronics_catalog := data.products.electronics
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
result := data.test.products[input.rule][input.item]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: 1200
|
||||
allow_interpreter_incorrect_behavior: true
|
||||
|
||||
# Case 3d: Undefined path access returns undefined
|
||||
- note: undefined_path_access
|
||||
data: {}
|
||||
input:
|
||||
path2: "path"
|
||||
path3: "value"
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# This should access undefined data, returning undefined
|
||||
result := data.nonexistent[input.path2][input.path3]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "#undefined"
|
||||
|
||||
# Case that actually triggers VirtualDataDocumentLookup:
|
||||
# Path is a prefix of multiple rules
|
||||
- note: virtual_lookup_with_rule_prefix
|
||||
data:
|
||||
config:
|
||||
app: {"name": "MyApp", "version": "1.0"}
|
||||
input:
|
||||
submodule: "app"
|
||||
field: "name"
|
||||
modules:
|
||||
- |
|
||||
package test.config.app
|
||||
name := data.config.app.name
|
||||
version := data.config.app.version
|
||||
full_info := {"name": data.config.app.name, "version": data.config.app.version}
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# This should trigger VirtualDataDocumentLookup since data.test.config.app
|
||||
# is a prefix of multiple rules: data.test.config.app.name, data.test.config.app.version, etc.
|
||||
result := data.test.config[input.submodule][input.field]
|
||||
}
|
||||
query: data.test.main
|
||||
want_result: "MyApp"
|
||||
|
||||
# Case 4: Subobject case - evaluate all rules in a subobject and merge with data
|
||||
- note: subobject_case_multiple_rules
|
||||
data:
|
||||
users:
|
||||
alice: {"name": "Alice", "age": 30}
|
||||
bob: {"name": "Bob", "age": 25}
|
||||
permissions:
|
||||
alice: {"admin": true}
|
||||
bob: {"admin": false}
|
||||
modules:
|
||||
- |
|
||||
package test.users.alice
|
||||
profile := {"name": data.users.alice.name, "age": data.users.alice.age}
|
||||
is_admin := data.permissions.alice.admin
|
||||
- |
|
||||
package test.users.bob
|
||||
profile := {"name": data.users.bob.name, "age": data.users.bob.age}
|
||||
is_admin := data.permissions.bob.admin
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# This should trigger Case 4: all components consumed and we have a subobject
|
||||
# data.test.users should contain the evaluated rules from both alice and bob packages
|
||||
result := data.test.users
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
alice:
|
||||
profile: {"name": "Alice", "age": 30}
|
||||
is_admin: true
|
||||
bob:
|
||||
profile: {"name": "Bob", "age": 25}
|
||||
is_admin: false
|
||||
|
||||
# Case 4b: Nested subobject evaluation with cache hits
|
||||
- note: nested_subobject_with_cache_hits
|
||||
data:
|
||||
company:
|
||||
departments:
|
||||
engineering: {"budget": 1000000}
|
||||
marketing: {"budget": 500000}
|
||||
employees:
|
||||
alice: {"dept": "engineering", "salary": 100000}
|
||||
bob: {"dept": "marketing", "salary": 70000}
|
||||
charlie: {"dept": "engineering", "salary": 90000}
|
||||
modules:
|
||||
- |
|
||||
package test.company.departments.engineering
|
||||
total_budget := data.company.departments.engineering.budget
|
||||
employee_count := count([e | e := data.company.employees[_]; e.dept == "engineering"])
|
||||
avg_budget_per_employee := total_budget / employee_count
|
||||
- |
|
||||
package test.company.departments.marketing
|
||||
total_budget := data.company.departments.marketing.budget
|
||||
employee_count := count([e | e := data.company.employees[_]; e.dept == "marketing"])
|
||||
avg_budget_per_employee := total_budget / employee_count
|
||||
- |
|
||||
package test.company.employees.alice
|
||||
profile := data.company.employees.alice
|
||||
department_info := data.test.company.departments[profile.dept] # Should hit cache
|
||||
- |
|
||||
package test.company.employees.bob
|
||||
profile := data.company.employees.bob
|
||||
department_info := data.test.company.departments[profile.dept] # Should hit cache
|
||||
- |
|
||||
package test.company.employees.charlie
|
||||
profile := data.company.employees.charlie
|
||||
department_info := data.test.company.departments[profile.dept] # Should hit cache again
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# This creates nested subobject evaluations:
|
||||
# 1. data.test.company (subobject with departments and employees)
|
||||
# 2. data.test.company.departments (subobject with engineering and marketing)
|
||||
# 3. data.test.company.employees (subobject with alice, bob, charlie)
|
||||
# The departments should be cached and reused multiple times
|
||||
result := {
|
||||
"company_overview": data.test.company,
|
||||
"departments_only": data.test.company.departments, # Cache hit for departments
|
||||
"employees_only": data.test.company.employees, # Cache hit for employees
|
||||
"engineering_dept": data.test.company.departments.engineering # Cache hit for specific dept
|
||||
}
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
company_overview:
|
||||
departments:
|
||||
engineering:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
marketing:
|
||||
total_budget: 500000
|
||||
employee_count: 1
|
||||
avg_budget_per_employee: 500000
|
||||
employees:
|
||||
alice:
|
||||
profile: {"dept": "engineering", "salary": 100000}
|
||||
department_info:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
bob:
|
||||
profile: {"dept": "marketing", "salary": 70000}
|
||||
department_info:
|
||||
total_budget: 500000
|
||||
employee_count: 1
|
||||
avg_budget_per_employee: 500000
|
||||
charlie:
|
||||
profile: {"dept": "engineering", "salary": 90000}
|
||||
department_info:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
departments_only:
|
||||
engineering:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
marketing:
|
||||
total_budget: 500000
|
||||
employee_count: 1
|
||||
avg_budget_per_employee: 500000
|
||||
employees_only:
|
||||
alice:
|
||||
profile: {"dept": "engineering", "salary": 100000}
|
||||
department_info:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
bob:
|
||||
profile: {"dept": "marketing", "salary": 70000}
|
||||
department_info:
|
||||
total_budget: 500000
|
||||
employee_count: 1
|
||||
avg_budget_per_employee: 500000
|
||||
charlie:
|
||||
profile: {"dept": "engineering", "salary": 90000}
|
||||
department_info:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
engineering_dept:
|
||||
total_budget: 1000000
|
||||
employee_count: 2
|
||||
avg_budget_per_employee: 500000
|
||||
|
||||
# Test that function rules are excluded from virtual data document lookup
|
||||
- note: function_rules_excluded_from_virtual_lookup
|
||||
data:
|
||||
config:
|
||||
app_name: "TestApp"
|
||||
version: "1.0.0"
|
||||
modules:
|
||||
- |
|
||||
package test.config
|
||||
# Regular rule - should be accessible via virtual lookup
|
||||
application_info := {"name": data.config.app_name, "version": data.config.version}
|
||||
|
||||
# Function rule - should NOT be accessible via virtual lookup
|
||||
format_version(major, minor) := sprintf("%d.%d", [major, minor])
|
||||
|
||||
# Another regular rule - should be accessible
|
||||
app_status := "running"
|
||||
- |
|
||||
package test
|
||||
main := result if {
|
||||
# This should only include regular rules, not function rules
|
||||
# data.test.config should contain: application_info, app_status
|
||||
# but NOT: format_version (because it's a function rule)
|
||||
result := data.test.config
|
||||
}
|
||||
query: data.test.main
|
||||
want_result:
|
||||
application_info: {"name": "TestApp", "version": "1.0.0"}
|
||||
app_status: "running"
|
||||
# Note: format_version should NOT appear here since it's a function rule
|
||||
578
tests/rvm/rego/mod.rs
Normal file
578
tests/rvm/rego/mod.rs
Normal file
@@ -0,0 +1,578 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
#![cfg(feature = "rvm")]
|
||||
|
||||
use anyhow::Result;
|
||||
use regorus::languages::rego::compiler::Compiler;
|
||||
use regorus::rvm::program::{generate_tabular_assembly_listing, AssemblyListingConfig, Program};
|
||||
use regorus::rvm::tests::test_utils::test_round_trip_serialization;
|
||||
use regorus::rvm::vm::RegoVM;
|
||||
use regorus::test_utils::{check_output, process_value, value_or_vec_to_vec, ValueOrVec};
|
||||
use regorus::{CompiledPolicy, Engine, Rc, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use test_generator::test_resources;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct TestCase {
|
||||
pub data: Option<Value>,
|
||||
pub input: Option<ValueOrVec>,
|
||||
pub modules: Vec<String>,
|
||||
pub note: String,
|
||||
pub query: String,
|
||||
pub entry_points: Option<Vec<String>>,
|
||||
pub sort_bindings: Option<bool>,
|
||||
pub want_result: Option<ValueOrVec>,
|
||||
pub want_results: Option<Vec<ValueOrVec>>,
|
||||
pub want_prints: Option<Vec<String>>,
|
||||
pub no_result: Option<bool>,
|
||||
pub skip: Option<bool>,
|
||||
pub error: Option<String>,
|
||||
pub traces: Option<bool>,
|
||||
pub want_error: Option<String>,
|
||||
pub want_error_code: Option<String>,
|
||||
#[serde(default = "default_strict")]
|
||||
pub strict: bool,
|
||||
pub allow_interpreter_success: Option<bool>,
|
||||
pub allow_interpreter_incorrect_behavior: Option<bool>,
|
||||
}
|
||||
|
||||
fn default_strict() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct YamlTest {
|
||||
pub cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
fn render_program_listing(program: &Program) -> String {
|
||||
let config = AssemblyListingConfig::default();
|
||||
generate_tabular_assembly_listing(program, &config)
|
||||
}
|
||||
|
||||
fn dump_rvm_listing(case_note: &str, listing: &Option<String>) {
|
||||
if let Some(listing) = listing {
|
||||
eprintln!("\n===== RVM assembly for '{}' =====", case_note);
|
||||
eprintln!("{}", listing);
|
||||
eprintln!("===== End RVM assembly =====\n");
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! panic_with_listing {
|
||||
($listing:expr, $case_note:expr, $($arg:tt)*) => {{
|
||||
dump_rvm_listing($case_note, $listing);
|
||||
panic!($($arg)*);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! bail_with_listing {
|
||||
($listing:expr, $case_note:expr, $($arg:tt)*) => {{
|
||||
dump_rvm_listing($case_note, $listing);
|
||||
anyhow::bail!($($arg)*);
|
||||
}};
|
||||
}
|
||||
|
||||
fn should_run_test_case(case_note: &str) -> bool {
|
||||
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
|
||||
case_note.contains(&filter)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_and_run_rvm(
|
||||
compiled_policy: &CompiledPolicy,
|
||||
entrypoint: &str,
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
) -> anyhow::Result<Value> {
|
||||
let results = compile_and_run_rvm_with_all_entry_points(
|
||||
compiled_policy,
|
||||
&[entrypoint],
|
||||
data,
|
||||
input,
|
||||
listing_out,
|
||||
)?;
|
||||
results
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no result returned from VM"))
|
||||
}
|
||||
|
||||
fn compile_and_run_rvm_with_entry_points(
|
||||
compiled_policy: &CompiledPolicy,
|
||||
entry_points: &[&str],
|
||||
execute_entry_point: &str,
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
) -> anyhow::Result<Value> {
|
||||
let results = compile_and_run_rvm_with_all_entry_points(
|
||||
compiled_policy,
|
||||
entry_points,
|
||||
data,
|
||||
input,
|
||||
listing_out,
|
||||
)?;
|
||||
|
||||
if let Some(index) = entry_points
|
||||
.iter()
|
||||
.position(|ep| *ep == execute_entry_point)
|
||||
{
|
||||
results
|
||||
.get(index)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing entry point result"))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"entry point '{}' not found in {:?}",
|
||||
execute_entry_point,
|
||||
entry_points
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_and_run_rvm_with_all_entry_points(
|
||||
compiled_policy: &CompiledPolicy,
|
||||
entry_points: &[&str],
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
) -> anyhow::Result<Vec<Value>> {
|
||||
let program = Compiler::compile_from_policy(compiled_policy, entry_points)?;
|
||||
|
||||
// Basic serialization sanity check keeps regressions visible in CI.
|
||||
test_round_trip_serialization(program.as_ref()).map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
*listing_out = Some(render_program_listing(program.as_ref()));
|
||||
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(program);
|
||||
vm.set_data(data.clone())?;
|
||||
vm.set_input(input.clone());
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (idx, _) in entry_points.iter().enumerate() {
|
||||
let result = if entry_points.len() == 1 {
|
||||
vm.execute()?
|
||||
} else {
|
||||
vm.execute_entry_point_by_index(idx)?
|
||||
};
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
let yaml_str = fs::read_to_string(file)?;
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
|
||||
|
||||
println!("running {file}");
|
||||
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
|
||||
println!("🔍 Test case filter active: '{filter}'");
|
||||
}
|
||||
|
||||
let mut executed_count = 0usize;
|
||||
let mut skipped_count = 0usize;
|
||||
|
||||
for case in test.cases {
|
||||
let mut last_listing: Option<String> = None;
|
||||
if !should_run_test_case(&case.note) {
|
||||
println!("case {} filtered out", case.note);
|
||||
skipped_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
print!("case {} ", case.note);
|
||||
|
||||
if case.skip == Some(true) {
|
||||
println!("skipped");
|
||||
skipped_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
executed_count += 1;
|
||||
|
||||
let mut engine = Engine::new();
|
||||
for (idx, module) in case.modules.iter().enumerate() {
|
||||
engine.add_policy(format!("rego_{idx}"), module.clone())?;
|
||||
}
|
||||
|
||||
if let Some(data) = case.data {
|
||||
engine.add_data(data)?;
|
||||
}
|
||||
|
||||
let input_value = case
|
||||
.input
|
||||
.clone()
|
||||
.map(|i| match i {
|
||||
ValueOrVec::Single(v) => v,
|
||||
ValueOrVec::Many(_) => Value::Null,
|
||||
})
|
||||
.unwrap_or(Value::Null);
|
||||
|
||||
if case.input.is_some() {
|
||||
engine.set_input(input_value.clone());
|
||||
}
|
||||
|
||||
let entrypoint_ref = Rc::from(case.query.as_str());
|
||||
let compilation_result = engine.compile_with_entrypoint(&entrypoint_ref);
|
||||
let data = engine.get_data();
|
||||
let interpreter_result = engine.eval_rule(case.query.clone());
|
||||
|
||||
if let Err(compilation_error) = &compilation_result {
|
||||
if let (None, Some(expected_error)) = (&case.want_result, &case.want_error) {
|
||||
let error_str = compilation_error.to_string();
|
||||
if error_str.contains(expected_error) {
|
||||
println!(
|
||||
"✓ RVM compilation error matches expected for case '{}'",
|
||||
case.note
|
||||
);
|
||||
println!("passed");
|
||||
continue;
|
||||
}
|
||||
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM compilation error does not match expected for case '{}':\nExpected: '{expected_error}'\nActual: '{error_str}'",
|
||||
case.note
|
||||
);
|
||||
}
|
||||
|
||||
dump_rvm_listing(&case.note, &last_listing);
|
||||
return Err(anyhow::anyhow!("Compilation failed: {compilation_error}"));
|
||||
}
|
||||
|
||||
let compiled_policy = compilation_result.unwrap();
|
||||
|
||||
if let Some(expected_results) = &case.want_results {
|
||||
if case.want_result.is_some() {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Cannot specify both want_result and want_results for case '{}'",
|
||||
case.note
|
||||
);
|
||||
}
|
||||
if case.want_error.is_some() {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Cannot specify both want_results and want_error for case '{}'",
|
||||
case.note
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref entry_points) = case.entry_points {
|
||||
let entry_point_refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect();
|
||||
match compile_and_run_rvm_with_all_entry_points(
|
||||
&compiled_policy,
|
||||
&entry_point_refs,
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
) {
|
||||
Ok(actual_results) => {
|
||||
if actual_results.len() != expected_results.len() {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Expected {} results, but got {} for case '{}'",
|
||||
expected_results.len(),
|
||||
actual_results.len(),
|
||||
case.note
|
||||
);
|
||||
}
|
||||
|
||||
for (index, (actual, expected)) in actual_results
|
||||
.iter()
|
||||
.zip(expected_results.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let expected_value = match expected {
|
||||
ValueOrVec::Single(v) => v.clone(),
|
||||
ValueOrVec::Many(vec) if vec.len() == 1 => vec[0].clone(),
|
||||
ValueOrVec::Many(_) => {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Unexpected multiple expected values for result {} in case '{}'",
|
||||
index,
|
||||
case.note
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let processed_expected = process_value(&expected_value)?;
|
||||
if *actual != processed_expected {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Result {} mismatch for case '{}': expected {:?}, got {:?}",
|
||||
index,
|
||||
case.note,
|
||||
processed_expected,
|
||||
actual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"✓ All {} entry point results match expected values for case '{}'",
|
||||
actual_results.len(),
|
||||
case.note
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Multiple entry points execution failed for case '{}': {}",
|
||||
case.note,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bail_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"want_results specified but no entry_points provided for case '{}'",
|
||||
case.note
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match (&case.want_result, &case.want_error) {
|
||||
(Some(expected_result), None) => {
|
||||
let result = if let Some(ref entry_points) = case.entry_points {
|
||||
let refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect();
|
||||
compile_and_run_rvm_with_entry_points(
|
||||
&compiled_policy,
|
||||
&refs,
|
||||
&case.query,
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
)
|
||||
} else {
|
||||
compile_and_run_rvm(
|
||||
&compiled_policy,
|
||||
&case.query,
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(actual_result) => {
|
||||
match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
if actual_result != *interpreter_value {
|
||||
if case.allow_interpreter_incorrect_behavior == Some(true) {
|
||||
println!(
|
||||
"✓ RVM result differs from interpreter for case '{}' (allowed)",
|
||||
case.note
|
||||
);
|
||||
} else {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM result does not match interpreter result for case '{}':\nRVM: {:?}\nInterpreter: {:?}",
|
||||
case.note,
|
||||
actual_result,
|
||||
interpreter_value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Interpreter failed for case '{}' but RVM succeeded:\nRVM result: {:?}\nInterpreter error: {}",
|
||||
case.note,
|
||||
actual_result,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let expected_results = value_or_vec_to_vec(expected_result.clone());
|
||||
let actual_results = vec![actual_result];
|
||||
check_output(&actual_results, &expected_results)?;
|
||||
}
|
||||
Err(e) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
if case.allow_interpreter_success == Some(true) {
|
||||
println!(
|
||||
"✓ RVM detected conflict for case '{}' (interpreter success allowed): {}",
|
||||
case.note,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM failed for case '{}' but interpreter succeeded:\nRVM error: {}\nInterpreter result: {:?}",
|
||||
case.note,
|
||||
e,
|
||||
interpreter_value
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Both RVM and interpreter failed for case '{}' but a result was expected:\nInterpreter error: {:?}\nRVM error: {}",
|
||||
case.note,
|
||||
err,
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
(None, Some(expected_error)) => {
|
||||
let result = if let Some(ref entry_points) = case.entry_points {
|
||||
let refs: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect();
|
||||
compile_and_run_rvm_with_entry_points(
|
||||
&compiled_policy,
|
||||
&refs,
|
||||
&case.query,
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
)
|
||||
} else {
|
||||
compile_and_run_rvm(
|
||||
&compiled_policy,
|
||||
&case.query,
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(result) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Test case '{}' expected error '{}' but both RVM and interpreter succeeded:\nRVM result: {}\nInterpreter result: {:?}",
|
||||
case.note,
|
||||
expected_error,
|
||||
serde_json::to_string_pretty(&result)?,
|
||||
interpreter_value
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Test case '{}' expected error '{}' but RVM succeeded while interpreter failed:\nRVM result: {}",
|
||||
case.note,
|
||||
expected_error,
|
||||
serde_json::to_string_pretty(&result)?
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(actual_error) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
if case.allow_interpreter_success == Some(true) {
|
||||
let actual_error_str = actual_error.to_string();
|
||||
if !actual_error_str.contains(expected_error) {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Error message mismatch for case '{}': expected contains '{}', actual '{}'",
|
||||
case.note,
|
||||
expected_error,
|
||||
actual_error_str
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"✓ RVM error matches expected for case '{}' (interpreter success allowed)",
|
||||
case.note
|
||||
);
|
||||
} else {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM failed for case '{}' but interpreter succeeded:\nRVM error: {}\nInterpreter result: {:?}",
|
||||
case.note,
|
||||
actual_error,
|
||||
interpreter_value
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let actual_error_str = actual_error.to_string();
|
||||
if !actual_error_str.contains(expected_error) {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Error message mismatch for case '{}': expected contains '{}', actual '{}'",
|
||||
case.note,
|
||||
expected_error,
|
||||
actual_error_str
|
||||
);
|
||||
}
|
||||
println!("✓ RVM error matches expected for case '{}'", case.note);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Test case '{}' must specify either want_result or want_error",
|
||||
case.note
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("passed");
|
||||
}
|
||||
|
||||
println!(
|
||||
"📊 Test Summary for {}: {} executed, {} skipped",
|
||||
file, executed_count, skipped_count
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_resources("tests/rvm/rego/cases/*.yaml")]
|
||||
fn run_rego_compiler_yaml(file: &str) {
|
||||
yaml_test_impl(file).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_specific_case() {
|
||||
if std::env::var("TEST_CASE_FILTER").is_err() {
|
||||
println!("💡 Specific case test skipped - no TEST_CASE_FILTER set");
|
||||
println!(" Usage: TEST_CASE_FILTER=\"note substring\" cargo test test_specific_case -- --nocapture");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir("tests/rvm/rego/cases") {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("yaml") {
|
||||
if let Err(e) = yaml_test_impl(path.to_str().unwrap()) {
|
||||
println!("❌ Error in file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user