#![allow(
clippy::panic,
clippy::panic_in_result_fn,
clippy::unwrap_used,
clippy::manual_assert,
clippy::indexing_slicing,
clippy::option_if_let_else,
clippy::semicolon_if_nothing_returned,
clippy::unseparated_literal_suffix,
clippy::use_debug,
clippy::unused_trait_names,
clippy::as_conversions,
clippy::pattern_type_mismatch
)]
#[cfg(test)]
mod tests {
use crate::rvm::tests::instruction_parser::{parse_instruction, parse_loop_mode};
use crate::rvm::tests::test_utils::test_round_trip_serialization;
use crate::utils::limits::ExecutionTimerConfig;
use core::num::NonZeroU32;
use core::time::Duration;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
struct RuleInfoSpec {
rule_type: String,
definitions: Vec<Vec<u16>>,
#[serde(default)]
default_rule_index: Option<u16>,
#[serde(default)]
default_literal_index: Option<u16>,
#[serde(default)]
destructuring_blocks: Option<Vec<Option<u16>>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct DefaultRuleSpec {
rule_name: String,
default_value: crate::Value,
}
use crate::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason, VmError};
use crate::test_utils::process_value;
use crate::value::Value;
use alloc::collections::{BTreeMap, VecDeque};
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use test_generator::test_resources;
extern crate alloc;
extern crate std;
#[derive(Debug, Clone, Deserialize, Serialize)]
struct HostAwaitResponseSpec {
id: crate::Value,
#[serde(default)]
value: Option<crate::Value>,
#[serde(default)]
values: Vec<crate::Value>,
}
fn default_vm_test_execution_timer_config() -> ExecutionTimerConfig {
ExecutionTimerConfig {
limit: Duration::from_secs(1),
check_interval: NonZeroU32::new(100).unwrap_or(NonZeroU32::MIN),
}
}
#[derive(Debug, Deserialize, Serialize)]
struct VmTestCase {
note: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
example_rego: Option<String>,
#[serde(default)]
data: Option<crate::Value>,
#[serde(default)]
input: Option<crate::Value>,
literals: Vec<crate::Value>,
#[serde(default)]
rule_infos: Vec<RuleInfoSpec>,
#[serde(default)]
default_rules: Vec<DefaultRuleSpec>,
#[serde(default)]
rule_tree: Option<crate::Value>,
#[serde(default)]
instruction_params: Option<InstructionParamsSpec>,
#[serde(default)]
max_instructions: Option<usize>,
#[serde(default)]
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
#[serde(default)]
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
#[serde(default)]
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
#[serde(default)]
ignore_run_to_completion_hostawait_failure: bool,
instructions: Vec<String>,
#[serde(default, deserialize_with = "deserialize_optional_value")]
want_result: Option<crate::Value>,
#[serde(default)]
want_error: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_value")]
want_result_strict: Option<crate::Value>,
#[serde(default)]
want_error_strict: Option<String>,
}
fn deserialize_optional_value<'de, D>(deserializer: D) -> Result<Option<crate::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
crate::Value::deserialize(deserializer).map(Some)
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
struct InstructionParamsSpec {
#[serde(default)]
loop_params: Vec<LoopStartParamsSpec>,
#[serde(default)]
call_params: Vec<CallParamsSpec>,
#[serde(default)]
builtin_call_params: Vec<BuiltinCallParamsSpec>,
#[serde(default)]
function_call_params: Vec<FunctionCallParamsSpec>,
#[serde(default)]
builtin_infos: Vec<BuiltinInfoSpec>,
#[serde(default)]
object_create_params: Vec<ObjectCreateParamsSpec>,
#[serde(default)]
array_create_params: Vec<ArrayCreateParamsSpec>,
#[serde(default)]
set_create_params: Vec<SetCreateParamsSpec>,
#[serde(default)]
virtual_data_document_lookup_params: Vec<VirtualDataDocumentLookupParamsSpec>,
#[serde(default)]
chained_index_params: Vec<ChainedIndexParamsSpec>,
#[serde(default, alias = "comprehension_start_params")]
comprehension_begin_params: Vec<ComprehensionBeginParamsSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct LoopStartParamsSpec {
mode: String,
collection: u16,
key_reg: u16,
value_reg: u16,
result_reg: u16,
body_start: u16,
loop_end: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct CallParamsSpec {
dest: u16,
func: u16,
args_start: u16,
args_count: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct BuiltinCallParamsSpec {
dest: u16,
builtin_index: u16,
args: Vec<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct FunctionCallParamsSpec {
func: u16,
dest: u16,
args: Vec<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct BuiltinInfoSpec {
name: String,
num_args: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ObjectCreateParamsSpec {
dest: u16,
template_literal_idx: u16,
literal_key_fields: Vec<(u16, u16)>,
fields: Vec<(u16, u16)>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ArrayCreateParamsSpec {
dest: u16,
elements: Vec<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct SetCreateParamsSpec {
dest: u16,
elements: Vec<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct LiteralOrRegisterSpec {
#[serde(default, alias = "literal")]
literal_idx: Option<u16>,
#[serde(default, alias = "reg")]
register: Option<u16>,
}
impl LiteralOrRegisterSpec {
fn into_literal_or_register(self) -> crate::rvm::instructions::LiteralOrRegister {
if let Some(literal_idx) = self.literal_idx {
crate::rvm::instructions::LiteralOrRegister::Literal(literal_idx)
} else if let Some(register) = self.register {
crate::rvm::instructions::LiteralOrRegister::Register(register.try_into().unwrap())
} else {
panic!("LiteralOrRegisterSpec must specify either literal_idx or register");
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct VirtualDataDocumentLookupParamsSpec {
dest: u16,
path_components: Vec<LiteralOrRegisterSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ChainedIndexParamsSpec {
dest: u16,
root: u16,
path_components: Vec<LiteralOrRegisterSpec>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ComprehensionBeginParamsSpec {
mode: String,
collection_reg: u16,
#[serde(default)]
result_reg: Option<u16>,
key_reg: u16,
value_reg: u16,
body_start: u16,
comprehension_end: u16,
}
#[derive(Debug, Deserialize, Serialize)]
struct VmTestSuite {
cases: Vec<VmTestCase>,
}
#[allow(clippy::too_many_arguments)]
fn execute_vm_instructions(
instructions: Vec<crate::rvm::instructions::Instruction>,
literals: Vec<Value>,
rule_infos: Vec<RuleInfoSpec>,
rule_tree: Option<Value>,
instruction_params: Option<InstructionParamsSpec>,
data: Option<Value>,
input: Option<Value>,
max_instructions: Option<usize>,
host_await_responses: Option<Vec<HostAwaitResponseSpec>>,
host_await_responses_run_to_completion: Option<Vec<HostAwaitResponseSpec>>,
host_await_responses_suspendable: Option<Vec<HostAwaitResponseSpec>>,
ignore_run_to_completion_hostawait_failure: bool,
strict: bool,
) -> Result<Value> {
let processed_data = if let Some(ref data_value) = data {
Some(process_value(data_value)?)
} else {
None
};
let processed_input = if let Some(ref input_value) = input {
Some(process_value(input_value)?)
} else {
None
};
let processed_rule_tree = if let Some(ref tree_value) = rule_tree {
Some(process_value(tree_value)?)
} else {
None
};
let process_responses =
|responses: Vec<HostAwaitResponseSpec>| -> Result<BTreeMap<Value, Vec<Value>>> {
let mut processed: BTreeMap<Value, Vec<Value>> = BTreeMap::new();
for spec in responses {
let identifier = process_value(&spec.id)?;
let mut values: Vec<Value> = Vec::new();
if let Some(single) = spec.value.as_ref() {
values.push(process_value(single)?);
}
for value in &spec.values {
values.push(process_value(value)?);
}
if values.is_empty() {
return Err(anyhow::anyhow!(
"HostAwait response specification for id {:?} has no values",
identifier
));
}
processed.entry(identifier).or_default().extend(values);
}
Ok(processed)
};
let processed_host_responses = if let Some(responses) = host_await_responses {
Some(process_responses(responses)?)
} else {
None
};
let processed_host_responses_run_to_completion =
if let Some(responses) = host_await_responses_run_to_completion {
Some(process_responses(responses)?)
} else {
processed_host_responses.clone()
};
let processed_host_responses_suspendable =
if let Some(responses) = host_await_responses_suspendable {
Some(process_responses(responses)?)
} else {
processed_host_responses.clone()
};
let mut program = crate::rvm::program::Program::new();
program.instructions = instructions;
let mut processed_literals = Vec::new();
for literal in &literals {
processed_literals.push(process_value(literal)?);
}
program.literals = processed_literals;
if let Some(tree) = processed_rule_tree {
program.rule_tree = tree;
} else {
program.rule_tree = Value::new_object();
}
for rule_info_spec in rule_infos.iter() {
use crate::rvm::program::{RuleInfo, RuleType};
let rule_type = match rule_info_spec.rule_type.as_str() {
"Complete" => RuleType::Complete,
"PartialSet" => RuleType::PartialSet,
"PartialObject" => RuleType::PartialObject,
_ => {
return Err(anyhow::anyhow!(
"Unknown rule type: {}",
rule_info_spec.rule_type
))
}
};
let definitions: Vec<Vec<u32>> = rule_info_spec
.definitions
.iter()
.map(|def| def.iter().map(|&x| x as u32).collect())
.collect();
let mut destructuring_blocks: Vec<Option<u32>> = rule_info_spec
.destructuring_blocks
.clone()
.map(|blocks| {
blocks
.into_iter()
.map(|entry| entry.map(|value| value as u32))
.collect()
})
.unwrap_or_else(|| alloc::vec![None; definitions.len()]);
if destructuring_blocks.len() != definitions.len() {
destructuring_blocks.resize(definitions.len(), None);
}
let result_reg = if instruction_params
.as_ref()
.is_some_and(|params| !params.function_call_params.is_empty())
{
0 } else {
1 };
let rule_info = RuleInfo {
name: String::from("test_rule"),
rule_type,
definitions: crate::Rc::new(definitions.clone()),
function_info: None,
default_literal_index: rule_info_spec.default_literal_index,
result_reg,
num_registers: 50, destructuring_blocks,
};
program.rule_infos.push(rule_info);
}
if let Some(params_spec) = instruction_params {
for loop_param_spec in params_spec.loop_params {
let mode = parse_loop_mode(&loop_param_spec.mode)?;
let loop_params = crate::rvm::instructions::LoopStartParams {
mode,
collection: loop_param_spec.collection.try_into().unwrap(),
key_reg: loop_param_spec.key_reg.try_into().unwrap(),
value_reg: loop_param_spec.value_reg.try_into().unwrap(),
result_reg: loop_param_spec.result_reg.try_into().unwrap(),
body_start: loop_param_spec.body_start,
loop_end: loop_param_spec.loop_end,
};
program.add_loop_params(loop_params);
}
if !params_spec.call_params.is_empty() {
panic!("Legacy call_params are no longer supported. Use builtin_call_params or function_call_params instead.");
}
for builtin_info_spec in params_spec.builtin_infos {
let builtin_info = crate::rvm::program::BuiltinInfo {
name: builtin_info_spec.name,
num_args: builtin_info_spec.num_args,
};
program.add_builtin_info(builtin_info);
}
for builtin_call_spec in params_spec.builtin_call_params {
use crate::rvm::instructions::BuiltinCallParams;
let mut args_array = [0u8; 8];
for (i, &arg) in builtin_call_spec.args.iter().enumerate() {
if i < 8 {
args_array[i] = arg.try_into().unwrap();
}
}
let builtin_call_params = BuiltinCallParams {
dest: builtin_call_spec.dest.try_into().unwrap(),
builtin_index: builtin_call_spec.builtin_index,
num_args: builtin_call_spec.args.len() as u8,
args: args_array,
};
program.add_builtin_call_params(builtin_call_params);
}
for function_call_spec in params_spec.function_call_params {
use crate::rvm::instructions::FunctionCallParams;
let mut args_array = [0u8; 8];
for (i, &arg) in function_call_spec.args.iter().enumerate() {
if i < 8 {
args_array[i] = arg.try_into().unwrap();
}
}
let function_call_params = FunctionCallParams {
func_rule_index: function_call_spec.func,
dest: function_call_spec.dest.try_into().unwrap(),
num_args: function_call_spec.args.len() as u8,
args: args_array,
};
program.add_function_call_params(function_call_params);
}
for object_create_spec in params_spec.object_create_params {
use crate::rvm::instructions::ObjectCreateParams;
let object_create_params = ObjectCreateParams {
dest: object_create_spec.dest.try_into().unwrap(),
template_literal_idx: object_create_spec.template_literal_idx,
literal_key_fields: object_create_spec
.literal_key_fields
.into_iter()
.map(|(k, v)| (k, v.try_into().unwrap()))
.collect(),
fields: object_create_spec
.fields
.into_iter()
.map(|(k, v)| (k.try_into().unwrap(), v.try_into().unwrap()))
.collect(),
};
program
.instruction_data
.add_object_create_params(object_create_params);
}
for array_create_spec in params_spec.array_create_params {
use crate::rvm::instructions::ArrayCreateParams;
let array_create_params = ArrayCreateParams {
dest: array_create_spec.dest.try_into().unwrap(),
elements: array_create_spec
.elements
.into_iter()
.map(|reg| reg.try_into().unwrap())
.collect(),
};
program
.instruction_data
.add_array_create_params(array_create_params);
}
for set_create_spec in params_spec.set_create_params {
use crate::rvm::instructions::SetCreateParams;
let set_create_params = SetCreateParams {
dest: set_create_spec.dest.try_into().unwrap(),
elements: set_create_spec
.elements
.into_iter()
.map(|reg| reg.try_into().unwrap())
.collect(),
};
program
.instruction_data
.add_set_create_params(set_create_params);
}
for virtual_spec in params_spec.virtual_data_document_lookup_params {
use crate::rvm::instructions::{
LiteralOrRegister, VirtualDataDocumentLookupParams,
};
let path_components: Vec<LiteralOrRegister> = virtual_spec
.path_components
.into_iter()
.map(|component| component.into_literal_or_register())
.collect();
let params = VirtualDataDocumentLookupParams {
dest: virtual_spec.dest.try_into().unwrap(),
path_components,
};
program
.instruction_data
.add_virtual_data_document_lookup_params(params);
}
for chained_spec in params_spec.chained_index_params {
use crate::rvm::instructions::{ChainedIndexParams, LiteralOrRegister};
let path_components: Vec<LiteralOrRegister> = chained_spec
.path_components
.into_iter()
.map(|component| component.into_literal_or_register())
.collect();
let params = ChainedIndexParams {
dest: chained_spec.dest.try_into().unwrap(),
root: chained_spec.root.try_into().unwrap(),
path_components,
};
program.instruction_data.add_chained_index_params(params);
}
for comprehension_spec in params_spec.comprehension_begin_params {
use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode};
let mode = match comprehension_spec.mode.as_str() {
"Array" => ComprehensionMode::Array,
"Set" => ComprehensionMode::Set,
"Object" => ComprehensionMode::Object,
_ => panic!("Invalid comprehension mode: {}", comprehension_spec.mode),
};
let comprehension_params = ComprehensionBeginParams {
mode,
collection_reg: comprehension_spec.collection_reg.try_into().unwrap(),
result_reg: comprehension_spec
.result_reg
.unwrap_or(comprehension_spec.collection_reg)
.try_into()
.unwrap(),
key_reg: comprehension_spec.key_reg.try_into().unwrap(),
value_reg: comprehension_spec.value_reg.try_into().unwrap(),
body_start: comprehension_spec.body_start,
comprehension_end: comprehension_spec.comprehension_end,
};
program
.instruction_data
.add_comprehension_begin_params(comprehension_params);
}
}
program.main_entry_point = 0;
program.max_rule_window_size = 255;
program.dispatch_window_size = 50;
if !program.builtin_info_table.is_empty() {
if let Err(e) = program.initialize_resolved_builtins() {
return Err(anyhow::anyhow!(
"Failed to initialize resolved builtins: {}",
e
));
}
}
test_round_trip_serialization(&program)
.map_err(|e| anyhow::anyhow!("Program serialization round-trip failed: {}", e))?;
let program = Arc::new(program);
let run_with_mode = |mode: ExecutionMode,
use_step_mode: bool,
host_responses_template: Option<BTreeMap<Value, Vec<Value>>>|
-> Result<Result<Value>> {
let mut vm = RegoVM::new();
vm.set_execution_mode(mode);
vm.set_step_mode(use_step_mode);
vm.set_strict_builtin_errors(strict);
vm.set_execution_timer_config(Some(default_vm_test_execution_timer_config()));
if let Some(data_value) = processed_data.clone() {
vm.set_data(data_value)?;
}
if let Some(input_value) = processed_input.clone() {
vm.set_input(input_value);
}
if let Some(limit) = max_instructions {
vm.set_max_instructions(limit);
}
if matches!(mode, ExecutionMode::RunToCompletion) {
if let Some(responses) = host_responses_template.clone() {
vm.set_host_await_responses(responses);
}
}
vm.load_program(program.clone());
let mut response_map = host_responses_template.clone().map(|map| {
map.into_iter()
.map(|(identifier, values)| (identifier, VecDeque::from(values)))
.collect::<BTreeMap<_, _>>()
});
let mut last_result = vm.execute().map_err(|e| anyhow::anyhow!("{}", e));
loop {
match vm.execution_state() {
ExecutionState::Completed { result } => {
return Ok(Ok(result.clone()));
}
ExecutionState::Error { error } => {
return Ok(Err(anyhow::anyhow!("{}", error)));
}
ExecutionState::Suspended { reason, .. } => {
if mode != ExecutionMode::Suspendable {
return Ok(Err(anyhow::anyhow!(
"Run-to-completion execution unexpectedly suspended: {:?}",
reason
)));
}
match reason {
SuspendReason::HostAwait {
dest, identifier, ..
} => {
let dest = *dest;
let identifier = identifier.clone();
let response = {
let map = response_map.as_mut().ok_or_else(|| {
anyhow::anyhow!(
"{}",
VmError::HostAwaitResponseMissing {
dest,
identifier: identifier.clone(),
pc: 0,
}
)
})?;
let queue = map.get_mut(&identifier).ok_or_else(|| {
anyhow::anyhow!(
"{}",
VmError::HostAwaitResponseMissing {
dest,
identifier: identifier.clone(),
pc: 0,
}
)
})?;
let response = queue.pop_front().ok_or_else(|| {
anyhow::anyhow!(
"{}",
VmError::HostAwaitResponseMissing {
dest,
identifier: identifier.clone(),
pc: 0,
}
)
})?;
if queue.is_empty() {
map.remove(&identifier);
}
response
};
last_result = vm
.resume(Some(response))
.map_err(|e| anyhow::anyhow!("{}", e));
}
SuspendReason::Step => {
if !use_step_mode {
return Ok(Err(anyhow::anyhow!(
"Suspendable execution unexpectedly suspended: {:?}",
reason
)));
}
last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e));
}
_ => {
last_result = vm.resume(None).map_err(|e| anyhow::anyhow!("{}", e));
}
}
}
_ => match &last_result {
Ok(value) => return Ok(Ok(value.clone())),
Err(err) => return Ok(Err(anyhow::anyhow!("{}", err))),
},
}
}
};
let compare_results = |baseline_name: &str,
baseline: &Result<Value>,
other_name: &str,
other: &Result<Value>|
-> Result<()> {
match (baseline, other) {
(Ok(expected), Ok(actual)) => {
if expected != actual {
return Err(anyhow::anyhow!(
"{} execution result {:?} differed from {} {:?}",
other_name,
actual,
baseline_name,
expected
));
}
Ok(())
}
(Err(expected_err), Err(other_err)) => {
let expected_msg = expected_err.to_string();
let other_msg = other_err.to_string();
if expected_msg != other_msg {
return Err(anyhow::anyhow!(
"{} execution error '{}' differed from {} '{}'",
other_name,
other_msg,
baseline_name,
expected_msg
));
}
Ok(())
}
(Ok(expected), Err(other_err)) => Err(anyhow::anyhow!(
"{} execution failed with '{}' while {} succeeded with {:?}",
other_name,
other_err,
baseline_name,
expected
)),
(Err(expected_err), Ok(actual)) => Err(anyhow::anyhow!(
"{} execution succeeded with {:?} while {} failed with '{}'",
other_name,
actual,
baseline_name,
expected_err
)),
}
};
let run_to_completion = run_with_mode(
ExecutionMode::RunToCompletion,
false,
processed_host_responses_run_to_completion.clone(),
)?;
let suspendable = run_with_mode(
ExecutionMode::Suspendable,
false,
processed_host_responses_suspendable.clone(),
)?;
let stepwise = run_with_mode(
ExecutionMode::Suspendable,
true,
processed_host_responses_suspendable.clone(),
)?;
const HOST_AWAIT_RESPONSE_MISSING: &str = "HostAwait executed but no response provided";
let ignore_run_to_completion = ignore_run_to_completion_hostawait_failure
&& matches!(
&run_to_completion,
Err(err) if err.to_string().contains(HOST_AWAIT_RESPONSE_MISSING)
);
if ignore_run_to_completion {
compare_results("suspendable", &suspendable, "step-by-step", &stepwise)?;
return suspendable;
}
compare_results(
"run-to-completion",
&run_to_completion,
"suspendable",
&suspendable,
)?;
compare_results(
"run-to-completion",
&run_to_completion,
"step-by-step",
&stepwise,
)?;
run_to_completion
}
fn run_vm_test_suite(file: &str) -> Result<()> {
std::println!("Running VM test suite: {}", file);
let yaml_content = fs::read_to_string(file)?;
let test_suite: VmTestSuite = serde_yaml::from_str(&yaml_content)?;
for test_case in test_suite.cases {
std::println!("Running VM test case: {}", test_case.note);
let instructions = test_case
.instructions
.iter()
.map(|instruction_str| parse_instruction(instruction_str))
.collect::<Result<Vec<_>>>()?;
let ignore_hostawait_failure = test_case.ignore_run_to_completion_hostawait_failure;
struct ModeExpectation<'a> {
strict: bool,
want_result: Option<&'a crate::Value>,
want_error: Option<&'a String>,
}
let mut expectations = Vec::new();
if test_case.want_result.is_some() || test_case.want_error.is_some() {
expectations.push(ModeExpectation {
strict: false,
want_result: test_case.want_result.as_ref(),
want_error: test_case.want_error.as_ref(),
});
}
if test_case.want_result_strict.is_some() || test_case.want_error_strict.is_some() {
expectations.push(ModeExpectation {
strict: true,
want_result: test_case.want_result_strict.as_ref(),
want_error: test_case.want_error_strict.as_ref(),
});
}
if expectations.is_empty() {
panic!(
"Test case '{}' must specify expectations for at least one mode",
test_case.note
);
}
for expectation in expectations {
let mode_label = if expectation.strict {
"strict"
} else {
"non-strict"
};
std::println!(" Mode: {}", mode_label);
let execution_result = execute_vm_instructions(
instructions.clone(),
test_case.literals.clone(),
test_case.rule_infos.clone(),
test_case.rule_tree.clone(),
test_case.instruction_params.clone(),
test_case.data.clone(),
test_case.input.clone(),
test_case.max_instructions,
test_case.host_await_responses.clone(),
test_case.host_await_responses_run_to_completion.clone(),
test_case.host_await_responses_suspendable.clone(),
ignore_hostawait_failure,
expectation.strict,
);
if expectation.want_error.is_some() && expectation.want_result.is_some() {
panic!(
"Test case '{}' cannot specify both want_result and want_error for {} mode",
test_case.note, mode_label
);
}
if let Some(expected_error) = expectation.want_error {
match execution_result {
Err(e) => {
let error_msg = std::format!("{}", e);
if !error_msg.contains(expected_error) {
std::println!(
"Test case '{}' failed ({} mode):",
test_case.note,
mode_label
);
std::println!(" Expected error containing: '{}'", expected_error);
std::println!(" Actual error: '{}'", error_msg);
panic!("VM test case failed: {}", test_case.note);
}
}
Ok(result) => {
std::println!(
"Test case '{}' failed ({} mode):",
test_case.note,
mode_label
);
std::println!(" Expected error containing: '{}'", expected_error);
std::println!(" But got successful result: {:?}", result);
panic!("VM test case failed: {}", test_case.note);
}
}
} else if let Some(want_result) = expectation.want_result {
let expected_result = process_value(want_result)?;
let actual_result = match execution_result {
Ok(result) => result,
Err(e) => {
if std::format!("{}", e).contains("Assertion failed") {
Value::Undefined
} else {
return Err(e);
}
}
};
if actual_result != expected_result {
std::println!(
"Test case '{}' failed ({} mode):",
test_case.note,
mode_label
);
std::println!(" Expected: {:?}", expected_result);
std::println!(" Actual: {:?}", actual_result);
panic!("VM test case failed: {}", test_case.note);
}
} else {
panic!(
"Test case '{}' must specify either want_result or want_error for {} mode",
test_case.note, mode_label
);
}
std::println!(" ✓ {} mode passed", mode_label);
}
std::println!("✓ Test case '{}' passed", test_case.note);
}
std::println!("✓ Test suite '{}' completed successfully", file);
Ok(())
}
#[test_resources("tests/rvm/vm/suites/*.yaml")]
fn run_vm_test_file(file: &str) {
run_vm_test_suite(file).unwrap()
}
#[test_resources("tests/rvm/vm/suites/loops/*.yaml")]
fn run_loop_test_file(file: &str) {
run_vm_test_suite(file).unwrap()
}
}