mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(bindings)!: add RVM/Program support across FFI and language bindings (#565)
- FFI: add RVM/Program APIs, execution state accessors, HostAwait handling, and buffer/result helpers in rvm.rs, common.rs, engine.rs. - Compiler: emit HostAwait for __builtin_host_await in function_calls.rs. - RVM tests: add HostAwait regression cases and extend harness for suspend/resume responses in host_await.yaml and mod.rs. - C/C++: add RVM tests/examples and wrapper updates in rvm_tests.c, rvm_tests.cpp, regorus.hpp, plus CMake wiring. - C#: add Program/Rvm bindings, SafeHandle/PInvoke, tests, and example usage in Regorus, RvmProgramTests.cs, Program.cs, and README updates. - Go: add Program/Rvm bindings, tests, and examples in rvm.go, rvm_test.go, main.go. - Java: add Program/Rvm bindings, JNI glue, and examples in lib.rs, regorus, Test.java. - Python: add Program/Rvm bindings and examples in lib.rs, test.py. - WASM: add Program/Rvm bindings and examples in lib.rs, test.js. - Tooling: wire binding tests in xtask and ignore generated Java artifacts in .gitignore. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
0316ccd90c
commit
3f7a5496dc
103
tests/rvm/rego/cases/host_await.yaml
Normal file
103
tests/rvm/rego/cases/host_await.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
cases:
|
||||
- note: host_await_run_to_completion
|
||||
data: {}
|
||||
input:
|
||||
enabled: true
|
||||
skip_interpreter: true
|
||||
execution_mode: run-to-completion
|
||||
modules:
|
||||
- |
|
||||
package demo
|
||||
import rego.v1
|
||||
|
||||
allow := [
|
||||
__builtin_host_await("ping", "id-1"),
|
||||
__builtin_host_await("pong", "id-2")
|
||||
] if {
|
||||
input.enabled
|
||||
}
|
||||
query: data.demo.allow
|
||||
host_await_responses:
|
||||
- id: "id-1"
|
||||
value: "response-1"
|
||||
- id: "id-2"
|
||||
value: "response-2"
|
||||
want_result: ["response-1", "response-2"]
|
||||
|
||||
- note: host_await_suspendable_queue
|
||||
data: {}
|
||||
input:
|
||||
enabled: true
|
||||
payloads: ["a", "b"]
|
||||
skip_interpreter: true
|
||||
execution_mode: suspendable
|
||||
modules:
|
||||
- |
|
||||
package demo
|
||||
import rego.v1
|
||||
|
||||
allow := [result |
|
||||
input.enabled
|
||||
payload := input.payloads[_]
|
||||
result := __builtin_host_await(payload, "queue")
|
||||
]
|
||||
query: data.demo.allow
|
||||
host_await_responses_suspendable:
|
||||
- id: "queue"
|
||||
value: "first"
|
||||
- id: "queue"
|
||||
value: "second"
|
||||
want_result: ["first", "second"]
|
||||
|
||||
- note: host_await_nested_rule
|
||||
data: {}
|
||||
input:
|
||||
enabled: true
|
||||
token: "alpha"
|
||||
skip_interpreter: true
|
||||
execution_mode: suspendable
|
||||
modules:
|
||||
- |
|
||||
package demo
|
||||
import rego.v1
|
||||
|
||||
allow if {
|
||||
input.enabled
|
||||
result := nested_result
|
||||
result == "ok"
|
||||
}
|
||||
|
||||
nested_result := __builtin_host_await(input.token, "nested")
|
||||
query: data.demo.allow
|
||||
host_await_responses_suspendable:
|
||||
- id: "nested"
|
||||
value: "ok"
|
||||
want_result: true
|
||||
|
||||
- note: host_await_nested_comprehension
|
||||
data: {}
|
||||
input:
|
||||
enabled: true
|
||||
items: ["x", "y"]
|
||||
skip_interpreter: true
|
||||
execution_mode: suspendable
|
||||
modules:
|
||||
- |
|
||||
package demo
|
||||
import rego.v1
|
||||
|
||||
allow := [outer |
|
||||
input.enabled
|
||||
inner := [r |
|
||||
item := input.items[_]
|
||||
r := __builtin_host_await(item, "nested-comp")
|
||||
]
|
||||
outer := inner
|
||||
]
|
||||
query: data.demo.allow
|
||||
host_await_responses_suspendable:
|
||||
- id: "nested-comp"
|
||||
value: "first"
|
||||
- id: "nested-comp"
|
||||
value: "second"
|
||||
want_result: [["first", "second"]]
|
||||
@@ -6,10 +6,11 @@ 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::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason};
|
||||
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::collections::{BTreeMap, VecDeque};
|
||||
use std::fs;
|
||||
use test_generator::test_resources;
|
||||
|
||||
@@ -35,6 +36,11 @@ struct TestCase {
|
||||
pub strict: bool,
|
||||
pub allow_interpreter_success: Option<bool>,
|
||||
pub allow_interpreter_incorrect_behavior: Option<bool>,
|
||||
pub skip_interpreter: Option<bool>,
|
||||
pub execution_mode: Option<String>,
|
||||
pub host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
|
||||
pub host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
|
||||
pub host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
|
||||
}
|
||||
|
||||
fn default_strict() -> bool {
|
||||
@@ -46,11 +52,86 @@ struct YamlTest {
|
||||
pub cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
struct HostAwaitResponseSpec {
|
||||
pub id: Value,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RvmExecutionOptions {
|
||||
execution_mode: ExecutionMode,
|
||||
host_await_responses_run_to_completion: Option<Vec<(Value, Vec<Value>)>>,
|
||||
host_await_responses_suspendable: Option<BTreeMap<Value, VecDeque<Value>>>,
|
||||
}
|
||||
|
||||
impl Default for RvmExecutionOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
execution_mode: ExecutionMode::RunToCompletion,
|
||||
host_await_responses_run_to_completion: None,
|
||||
host_await_responses_suspendable: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_program_listing(program: &Program) -> String {
|
||||
let config = AssemblyListingConfig::default();
|
||||
generate_tabular_assembly_listing(program, &config)
|
||||
}
|
||||
|
||||
fn build_host_await_response_map(
|
||||
responses: &[HostAwaitResponseSpec],
|
||||
) -> anyhow::Result<BTreeMap<Value, VecDeque<Value>>> {
|
||||
let mut map: BTreeMap<Value, VecDeque<Value>> = BTreeMap::new();
|
||||
for response in responses {
|
||||
let id = process_value(&response.id)?;
|
||||
let value = process_value(&response.value)?;
|
||||
map.entry(id).or_default().push_back(value);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn build_host_await_response_vec(
|
||||
responses: &[HostAwaitResponseSpec],
|
||||
) -> anyhow::Result<Vec<(Value, Vec<Value>)>> {
|
||||
let map = build_host_await_response_map(responses)?;
|
||||
Ok(map
|
||||
.into_iter()
|
||||
.map(|(id, values)| (id, values.into_iter().collect()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn build_execution_options(case: &TestCase) -> anyhow::Result<RvmExecutionOptions> {
|
||||
let execution_mode = match case.execution_mode.as_deref() {
|
||||
None | Some("run-to-completion") => ExecutionMode::RunToCompletion,
|
||||
Some("suspendable") => ExecutionMode::Suspendable,
|
||||
Some(other) => {
|
||||
return Err(anyhow::anyhow!("unsupported execution_mode: {other}"));
|
||||
}
|
||||
};
|
||||
|
||||
let rtc_responses = case
|
||||
.host_await_responses_run_to_completion
|
||||
.as_ref()
|
||||
.or(case.host_await_responses.as_ref())
|
||||
.map(|responses| build_host_await_response_vec(responses))
|
||||
.transpose()?;
|
||||
|
||||
let suspendable_responses = case
|
||||
.host_await_responses_suspendable
|
||||
.as_ref()
|
||||
.or(case.host_await_responses.as_ref())
|
||||
.map(|responses| build_host_await_response_map(responses))
|
||||
.transpose()?;
|
||||
|
||||
Ok(RvmExecutionOptions {
|
||||
execution_mode,
|
||||
host_await_responses_run_to_completion: rtc_responses,
|
||||
host_await_responses_suspendable: suspendable_responses,
|
||||
})
|
||||
}
|
||||
|
||||
fn dump_rvm_listing(case_note: &str, listing: &Option<String>) {
|
||||
if let Some(listing) = listing {
|
||||
eprintln!("\n===== RVM assembly for '{}' =====", case_note);
|
||||
@@ -87,6 +168,7 @@ fn compile_and_run_rvm(
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
execution_options: &RvmExecutionOptions,
|
||||
) -> anyhow::Result<Value> {
|
||||
let results = compile_and_run_rvm_with_all_entry_points(
|
||||
compiled_policy,
|
||||
@@ -94,6 +176,7 @@ fn compile_and_run_rvm(
|
||||
data,
|
||||
input,
|
||||
listing_out,
|
||||
execution_options,
|
||||
)?;
|
||||
results
|
||||
.into_iter()
|
||||
@@ -108,6 +191,7 @@ fn compile_and_run_rvm_with_entry_points(
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
execution_options: &RvmExecutionOptions,
|
||||
) -> anyhow::Result<Value> {
|
||||
let results = compile_and_run_rvm_with_all_entry_points(
|
||||
compiled_policy,
|
||||
@@ -115,6 +199,7 @@ fn compile_and_run_rvm_with_entry_points(
|
||||
data,
|
||||
input,
|
||||
listing_out,
|
||||
execution_options,
|
||||
)?;
|
||||
|
||||
if let Some(index) = entry_points
|
||||
@@ -140,6 +225,7 @@ fn compile_and_run_rvm_with_all_entry_points(
|
||||
data: &Value,
|
||||
input: &Value,
|
||||
listing_out: &mut Option<String>,
|
||||
execution_options: &RvmExecutionOptions,
|
||||
) -> anyhow::Result<Vec<Value>> {
|
||||
let program = Compiler::compile_from_policy(compiled_policy, entry_points)?;
|
||||
|
||||
@@ -153,9 +239,61 @@ fn compile_and_run_rvm_with_all_entry_points(
|
||||
vm.set_data(data.clone())?;
|
||||
vm.set_input(input.clone());
|
||||
|
||||
if execution_options.execution_mode == ExecutionMode::Suspendable {
|
||||
vm.set_execution_mode(ExecutionMode::Suspendable);
|
||||
}
|
||||
|
||||
if execution_options.execution_mode == ExecutionMode::RunToCompletion {
|
||||
if let Some(responses) = &execution_options.host_await_responses_run_to_completion {
|
||||
vm.set_host_await_responses(responses.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (idx, _) in entry_points.iter().enumerate() {
|
||||
let result = if entry_points.len() == 1 {
|
||||
let result = if execution_options.execution_mode == ExecutionMode::Suspendable {
|
||||
let mut suspendable_responses = execution_options
|
||||
.host_await_responses_suspendable
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
let _ = if entry_points.len() == 1 {
|
||||
vm.execute()?
|
||||
} else {
|
||||
vm.execute_entry_point_by_index(idx)?
|
||||
};
|
||||
|
||||
loop {
|
||||
match vm.execution_state() {
|
||||
ExecutionState::Completed { result } => break result.clone(),
|
||||
ExecutionState::Error { error } => {
|
||||
return Err(anyhow::anyhow!("{}", error));
|
||||
}
|
||||
ExecutionState::Suspended { reason, .. } => match reason {
|
||||
SuspendReason::HostAwait { identifier, .. } => {
|
||||
let response = suspendable_responses
|
||||
.get_mut(identifier)
|
||||
.and_then(|queue| queue.pop_front())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Missing HostAwait response for identifier {:?}",
|
||||
identifier
|
||||
)
|
||||
})?;
|
||||
vm.resume(Some(response))?;
|
||||
}
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unexpected suspension reason: {:?}",
|
||||
other
|
||||
));
|
||||
}
|
||||
},
|
||||
ExecutionState::Running | ExecutionState::Ready => {
|
||||
return Err(anyhow::anyhow!("VM stuck in running state"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if entry_points.len() == 1 {
|
||||
vm.execute()?
|
||||
} else {
|
||||
vm.execute_entry_point_by_index(idx)?
|
||||
@@ -201,8 +339,8 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
engine.add_policy(format!("rego_{idx}"), module.clone())?;
|
||||
}
|
||||
|
||||
if let Some(data) = case.data {
|
||||
engine.add_data(data)?;
|
||||
if let Some(ref data) = case.data {
|
||||
engine.add_data(data.clone())?;
|
||||
}
|
||||
|
||||
let input_value = case
|
||||
@@ -221,7 +359,13 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
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());
|
||||
let interpreter_result = if case.skip_interpreter == Some(true) {
|
||||
None
|
||||
} else {
|
||||
Some(engine.eval_rule(case.query.clone()))
|
||||
};
|
||||
|
||||
let execution_options = build_execution_options(&case)?;
|
||||
|
||||
if let Err(compilation_error) = &compilation_result {
|
||||
if let (None, Some(expected_error)) = (&case.want_result, &case.want_error) {
|
||||
@@ -275,6 +419,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
&execution_options,
|
||||
) {
|
||||
Ok(actual_results) => {
|
||||
if actual_results.len() != expected_results.len() {
|
||||
@@ -359,6 +504,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
&execution_options,
|
||||
)
|
||||
} else {
|
||||
compile_and_run_rvm(
|
||||
@@ -367,41 +513,52 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
&execution_options,
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(actual_result) => {
|
||||
match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
if actual_result != *interpreter_value {
|
||||
if let Some(interpreter_result) = &interpreter_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) => {
|
||||
if case.allow_interpreter_incorrect_behavior == Some(true) {
|
||||
println!(
|
||||
"✓ RVM result differs from interpreter for case '{}' (allowed)",
|
||||
case.note
|
||||
"✓ Interpreter failed for case '{}' but RVM succeeded (allowed): {}",
|
||||
case.note,
|
||||
err
|
||||
);
|
||||
} else {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM result does not match interpreter result for case '{}':\nRVM: {:?}\nInterpreter: {:?}",
|
||||
"Interpreter failed for case '{}' but RVM succeeded:\nRVM result: {:?}\nInterpreter error: {}",
|
||||
case.note,
|
||||
actual_result,
|
||||
interpreter_value
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
@@ -409,7 +566,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
check_output(&actual_results, &expected_results)?;
|
||||
}
|
||||
Err(e) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
Some(Ok(interpreter_value)) => {
|
||||
if case.allow_interpreter_success == Some(true) {
|
||||
println!(
|
||||
"✓ RVM detected conflict for case '{}' (interpreter success allowed): {}",
|
||||
@@ -427,7 +584,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
Some(Err(err)) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
@@ -437,6 +594,15 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
e
|
||||
);
|
||||
}
|
||||
None => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"RVM failed for case '{}' but a result was expected:\nRVM error: {}",
|
||||
case.note,
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -450,6 +616,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
&execution_options,
|
||||
)
|
||||
} else {
|
||||
compile_and_run_rvm(
|
||||
@@ -458,12 +625,13 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
&data,
|
||||
&input_value,
|
||||
&mut last_listing,
|
||||
&execution_options,
|
||||
)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(result) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
Some(Ok(interpreter_value)) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
@@ -474,7 +642,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
interpreter_value
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
Some(Err(_)) => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
@@ -484,9 +652,19 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
serde_json::to_string_pretty(&result)?
|
||||
);
|
||||
}
|
||||
None => {
|
||||
panic_with_listing!(
|
||||
&last_listing,
|
||||
&case.note,
|
||||
"Test case '{}' expected error '{}' but RVM succeeded:\nRVM result: {}",
|
||||
case.note,
|
||||
expected_error,
|
||||
serde_json::to_string_pretty(&result)?
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(actual_error) => match &interpreter_result {
|
||||
Ok(interpreter_value) => {
|
||||
Some(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) {
|
||||
@@ -514,7 +692,7 @@ fn yaml_test_impl(file: &str) -> Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Some(Err(_)) | None => {
|
||||
let actual_error_str = actual_error.to_string();
|
||||
if !actual_error_str.contains(expected_error) {
|
||||
panic_with_listing!(
|
||||
|
||||
Reference in New Issue
Block a user