diff --git a/release-plz.toml b/release-plz.toml index 35c2c87..0dea38d 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -11,3 +11,7 @@ git_tag_enable = true git_release_enable = true changelog_update = true publish = true + +[[package]] +name = "xtask" +release = false diff --git a/src/rvm/tests/vm.rs b/src/rvm/tests/vm.rs index ceeafd8..f293985 100644 --- a/src/rvm/tests/vm.rs +++ b/src/rvm/tests/vm.rs @@ -18,15 +18,9 @@ #[cfg(test)] mod tests { - use crate::rvm::program::Program; use crate::rvm::tests::instruction_parser::{parse_instruction, parse_loop_mode}; use crate::rvm::tests::test_utils::test_round_trip_serialization; - #[cfg(any(test, not(feature = "std")))] - use crate::utils::limits::set_time_source; - use crate::utils::limits::{ - acquire_limits_test_lock, fallback_execution_timer_config, - set_fallback_execution_timer_config, ExecutionTimerConfig, TimeSource, - }; + use crate::utils::limits::ExecutionTimerConfig; use core::num::NonZeroU32; use core::time::Duration; #[derive(Debug, Clone, Deserialize, Serialize, Default)] @@ -57,122 +51,11 @@ mod tests { use anyhow::Result; use serde::{Deserialize, Serialize}; use std::fs; - use std::sync::{Mutex, Once}; use test_generator::test_resources; extern crate alloc; extern crate std; - struct FallbackGuard(Option); - impl Drop for FallbackGuard { - fn drop(&mut self) { - set_fallback_execution_timer_config(self.0); - } - } - - fn install_fallback_config(config: Option) -> FallbackGuard { - let previous = fallback_execution_timer_config(); - set_fallback_execution_timer_config(config); - FallbackGuard(previous) - } - - struct TimeSourceGuard { - previous_default: Duration, - previous_template: Vec, - } - - impl Drop for TimeSourceGuard { - fn drop(&mut self) { - let mut state = TIME_SOURCE_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - state.default_increment = self.previous_default; - state.template_increments = self.previous_template.clone(); - state.reset_from_template(); - } - } - - fn configure_time_source( - increments: Vec, - default_increment: Duration, - ) -> TimeSourceGuard { - ensure_time_source_registered(); - - let mut state = TIME_SOURCE_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - - let guard = TimeSourceGuard { - previous_default: state.default_increment, - previous_template: state.template_increments.clone(), - }; - - state.default_increment = default_increment; - state.template_increments = increments; - state.reset_from_template(); - - guard - } - - struct TestTimeSource; - - struct TimeSourceState { - current: Duration, - started: bool, - default_increment: Duration, - increments: VecDeque, - template_increments: Vec, - } - - impl TimeSourceState { - const fn new() -> Self { - Self { - current: Duration::ZERO, - started: false, - default_increment: Duration::from_millis(1), - increments: VecDeque::new(), - template_increments: Vec::new(), - } - } - - fn reset_from_template(&mut self) { - self.current = Duration::ZERO; - self.started = false; - self.increments = VecDeque::from(self.template_increments.clone()); - } - } - - impl TimeSource for TestTimeSource { - fn now(&self) -> Option { - let mut state = TIME_SOURCE_STATE - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - - if !state.started { - state.started = true; - return Some(state.current); - } - - let increment = state - .increments - .pop_front() - .unwrap_or(state.default_increment); - state.current = state.current.saturating_add(increment); - Some(state.current) - } - } - - static TEST_TIME_SOURCE: TestTimeSource = TestTimeSource; - static TIME_SOURCE_STATE: Mutex = Mutex::new(TimeSourceState::new()); - static TIME_SOURCE_ONCE: Once = Once::new(); - - fn ensure_time_source_registered() { - #[cfg(any(test, not(feature = "std")))] - TIME_SOURCE_ONCE.call_once(|| { - let _ = set_time_source(&TEST_TIME_SOURCE); - }); - } - #[derive(Debug, Clone, Deserialize, Serialize)] struct HostAwaitResponseSpec { id: crate::Value, @@ -1129,158 +1012,6 @@ mod tests { Ok(()) } - #[test] - fn vm_execution_time_limit_triggers_error() -> Result<()> { - use crate::rvm::instructions::Instruction; - use crate::utils::limits::acquire_limits_test_lock; - use core::num::NonZeroU32; - use core::time::Duration; - - let _lock = acquire_limits_test_lock(); - let config = ExecutionTimerConfig { - limit: Duration::from_nanos(1), - check_interval: NonZeroU32::new(1).unwrap(), - }; - let _guard = install_fallback_config(Some(config)); - - let mut program = Program::new(); - program.dispatch_window_size = 2; - program.max_rule_window_size = 2; - program.entry_points.insert("main".to_string(), 0); - - const INSTRUCTION_COUNT: usize = 60_000; - program.instructions = (0..INSTRUCTION_COUNT) - .map(|_| Instruction::LoadNull { dest: 0 }) - .collect(); - program.instructions.push(Instruction::Return { value: 0 }); - program.instruction_spans = alloc::vec![None; program.instructions.len()]; - program.main_entry_point = 0; - - let program = Arc::new(program); - - let mut vm = RegoVM::new(); - vm.set_max_instructions(usize::MAX); - vm.load_program(program); - - let result = vm.execute(); - assert!( - matches!(result, Err(VmError::TimeLimitExceeded { .. })), - "expected time limit error but got {result:?}" - ); - - Ok(()) - } - - #[test] - fn vm_execution_time_limit_override_allows_completion() -> Result<()> { - use crate::rvm::instructions::Instruction; - use crate::utils::limits::acquire_limits_test_lock; - use core::num::NonZeroU32; - use core::time::Duration; - - let _lock = acquire_limits_test_lock(); - let strict_config = ExecutionTimerConfig { - limit: Duration::from_nanos(1), - check_interval: NonZeroU32::new(1).unwrap(), - }; - let _guard = install_fallback_config(Some(strict_config)); - - let mut program = Program::new(); - program.dispatch_window_size = 2; - program.max_rule_window_size = 2; - program.entry_points.insert("main".to_string(), 0); - program.instructions = alloc::vec![ - Instruction::LoadNull { dest: 0 }, - Instruction::Return { value: 0 }, - ]; - program.instruction_spans = alloc::vec![None; program.instructions.len()]; - program.main_entry_point = 0; - - let program = Arc::new(program); - - let mut vm = RegoVM::new(); - vm.load_program(program); - - let relaxed_config = ExecutionTimerConfig { - limit: Duration::from_millis(10), - check_interval: NonZeroU32::new(1).unwrap(), - }; - vm.set_execution_timer_config(Some(relaxed_config)); - - let result = vm.execute(); - assert!( - result.is_ok(), - "expected successful execution, got {result:?}" - ); - - Ok(()) - } - - #[test] - fn vm_suspend_resume_excludes_suspended_time_from_limit() -> Result<()> { - use crate::rvm::instructions::Instruction; - - let _lock = acquire_limits_test_lock(); - let _guard = install_fallback_config(Some(ExecutionTimerConfig { - limit: Duration::from_millis(10), - check_interval: NonZeroU32::new(1).unwrap(), - })); - - let _time_guard = configure_time_source( - alloc::vec![ - Duration::from_millis(1), - Duration::from_millis(1), - Duration::from_millis(1), - Duration::from_millis(1), - Duration::from_millis(100), - Duration::from_millis(1), - ], - Duration::from_millis(1), - ); - - let mut program = Program::new(); - program.dispatch_window_size = 3; - program.max_rule_window_size = 3; - program.entry_points.insert("main".to_string(), 0); - program.literals = alloc::vec![Value::from("id"), Value::from(1)]; - program.instructions = alloc::vec![ - Instruction::Load { - dest: 0, - literal_idx: 0 - }, - Instruction::Load { - dest: 1, - literal_idx: 1 - }, - Instruction::HostAwait { - dest: 2, - arg: 1, - id: 0 - }, - Instruction::Return { value: 2 }, - ]; - program.instruction_spans = alloc::vec![None; program.instructions.len()]; - program.main_entry_point = 0; - - let program = Arc::new(program); - let mut vm = RegoVM::new(); - vm.set_execution_mode(ExecutionMode::Suspendable); - vm.load_program(program); - - let _ = vm.execute()?; - match vm.execution_state() { - ExecutionState::Suspended { reason, .. } => { - assert!(matches!(reason, SuspendReason::HostAwait { .. })); - } - other => panic!("expected suspension, got {other:?}"), - } - - let resumed = vm.resume(Some(Value::from(42)))?; - assert_eq!(resumed, Value::from(42)); - - Ok(()) - } - #[test_resources("tests/rvm/vm/suites/*.yaml")] fn run_vm_test_file(file: &str) { run_vm_test_suite(file).unwrap() diff --git a/tests/execution_limits.rs b/tests/execution_limits.rs new file mode 100644 index 0000000..b5aa8ee --- /dev/null +++ b/tests/execution_limits.rs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![cfg(feature = "rvm")] + +use anyhow::Result; +use regorus::rvm::vm::{ExecutionMode, ExecutionState, SuspendReason, VmError}; +use regorus::rvm::{Instruction, Program, RegoVM}; +use regorus::utils::limits::{ + fallback_execution_timer_config, set_fallback_execution_timer_config, ExecutionTimerConfig, +}; +use regorus::Value; +use std::num::NonZeroU32; +use std::sync::Mutex; +use std::thread::sleep; +use std::time::Duration; + +static LIMITS_TEST_LOCK: Mutex<()> = Mutex::new(()); + +struct FallbackGuard(Option); + +impl Drop for FallbackGuard { + fn drop(&mut self) { + set_fallback_execution_timer_config(self.0); + } +} + +fn install_fallback_config(config: Option) -> FallbackGuard { + let previous = fallback_execution_timer_config(); + set_fallback_execution_timer_config(config); + FallbackGuard(previous) +} + +#[test] +fn vm_execution_time_limit_triggers_error() -> Result<()> { + let _lock = LIMITS_TEST_LOCK.lock().unwrap(); + let config = ExecutionTimerConfig { + limit: Duration::from_nanos(1), + check_interval: NonZeroU32::new(1).unwrap(), + }; + let _guard = install_fallback_config(Some(config)); + + let mut program = Program::new(); + program.dispatch_window_size = 2; + program.max_rule_window_size = 2; + program.entry_points.insert("main".to_string(), 0); + + const INSTRUCTION_COUNT: usize = 60_000; + program.instructions = (0..INSTRUCTION_COUNT) + .map(|_| Instruction::LoadNull { dest: 0 }) + .collect(); + program.instructions.push(Instruction::Return { value: 0 }); + program.instruction_spans = vec![None; program.instructions.len()]; + program.main_entry_point = 0; + + let program = std::sync::Arc::new(program); + + let mut vm = RegoVM::new(); + vm.set_max_instructions(usize::MAX); + vm.load_program(program); + + let result = vm.execute(); + assert!( + matches!(result, Err(VmError::TimeLimitExceeded { .. })), + "expected time limit error but got {result:?}" + ); + + Ok(()) +} + +#[test] +fn vm_execution_time_limit_override_allows_completion() -> Result<()> { + let _lock = LIMITS_TEST_LOCK.lock().unwrap(); + let strict_config = ExecutionTimerConfig { + limit: Duration::from_nanos(1), + check_interval: NonZeroU32::new(1).unwrap(), + }; + let _guard = install_fallback_config(Some(strict_config)); + + let mut program = Program::new(); + program.dispatch_window_size = 2; + program.max_rule_window_size = 2; + program.entry_points.insert("main".to_string(), 0); + program.instructions = vec![ + Instruction::LoadNull { dest: 0 }, + Instruction::Return { value: 0 }, + ]; + program.instruction_spans = vec![None; program.instructions.len()]; + program.main_entry_point = 0; + + let program = std::sync::Arc::new(program); + + let mut vm = RegoVM::new(); + vm.load_program(program); + + let relaxed_config = ExecutionTimerConfig { + limit: Duration::from_millis(100), + check_interval: NonZeroU32::new(1).unwrap(), + }; + vm.set_execution_timer_config(Some(relaxed_config)); + + let result = vm.execute(); + assert!( + result.is_ok(), + "expected successful execution, got {result:?}" + ); + + Ok(()) +} + +#[test] +fn vm_suspend_resume_excludes_suspended_time_from_limit() -> Result<()> { + let _lock = LIMITS_TEST_LOCK.lock().unwrap(); + let _guard = install_fallback_config(Some(ExecutionTimerConfig { + // Allow headroom for normal execution while still failing if suspended + // time is included in the timer budget. + limit: Duration::from_millis(500), + check_interval: NonZeroU32::new(1).unwrap(), + })); + + let mut program = Program::new(); + program.dispatch_window_size = 3; + program.max_rule_window_size = 3; + program.entry_points.insert("main".to_string(), 0); + program.literals = vec![Value::from("id"), Value::from(1)]; + program.instructions = vec![ + Instruction::Load { + dest: 0, + literal_idx: 0, + }, + Instruction::Load { + dest: 1, + literal_idx: 1, + }, + Instruction::HostAwait { + dest: 2, + arg: 1, + id: 0, + }, + Instruction::Return { value: 2 }, + ]; + program.instruction_spans = vec![None; program.instructions.len()]; + program.main_entry_point = 0; + + let program = std::sync::Arc::new(program); + let mut vm = RegoVM::new(); + vm.set_execution_mode(ExecutionMode::Suspendable); + vm.load_program(program); + + let _ = vm.execute()?; + match vm.execution_state() { + ExecutionState::Suspended { reason, .. } => { + assert!(matches!(reason, SuspendReason::HostAwait { .. })); + } + other => panic!("expected suspension, got {other:?}"), + } + + // Sleep longer than the limit; resume should still succeed if suspended time is excluded. + sleep(Duration::from_secs(1)); + + let resumed = vm.resume(Some(Value::from(42)))?; + assert_eq!(resumed, Value::from(42)); + + Ok(()) +}