mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
# 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>
396 lines
13 KiB
YAML
396 lines
13 KiB
YAML
# 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 |