mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
test(rvm): Move vm execution limit tests to a separate test to avoid flakiness (#558)
Having a separate integration test allows the execution tests to freely change the global fallback limits without affecting other tests. also ask release-plz to ignore xtask package Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
e688806ca0
commit
10eebfe54c
@@ -11,3 +11,7 @@ git_tag_enable = true
|
||||
git_release_enable = true
|
||||
changelog_update = true
|
||||
publish = true
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
release = false
|
||||
|
||||
@@ -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<ExecutionTimerConfig>);
|
||||
impl Drop for FallbackGuard {
|
||||
fn drop(&mut self) {
|
||||
set_fallback_execution_timer_config(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_fallback_config(config: Option<ExecutionTimerConfig>) -> FallbackGuard {
|
||||
let previous = fallback_execution_timer_config();
|
||||
set_fallback_execution_timer_config(config);
|
||||
FallbackGuard(previous)
|
||||
}
|
||||
|
||||
struct TimeSourceGuard {
|
||||
previous_default: Duration,
|
||||
previous_template: Vec<Duration>,
|
||||
}
|
||||
|
||||
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<Duration>,
|
||||
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<Duration>,
|
||||
template_increments: Vec<Duration>,
|
||||
}
|
||||
|
||||
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<Duration> {
|
||||
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<TimeSourceState> = 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()
|
||||
|
||||
165
tests/execution_limits.rs
Normal file
165
tests/execution_limits.rs
Normal file
@@ -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<ExecutionTimerConfig>);
|
||||
|
||||
impl Drop for FallbackGuard {
|
||||
fn drop(&mut self) {
|
||||
set_fallback_execution_timer_config(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_fallback_config(config: Option<ExecutionTimerConfig>) -> 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user