Files
endorphin-aster-vm/crates/core/tests/native_context.rs
T

271 lines
8.6 KiB
Rust

//! Remote-only regression coverage. Do not execute locally.
use endor_core::{
ChangeKey, ContextNativeFunction, ExecutionLimits, NativeCallable, NativeContext, NativeResult,
ObjectRef, Value, Vm, VmError,
};
#[derive(Debug)]
struct Allocate(bool);
impl NativeCallable for Allocate {
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
let value = cx.new_object()?;
cx.set_attr(value, "label", Value::Text("scoped".into()))?;
let _temporary = cx.new_object()?;
if self.0 {
return Err(VmError::InvalidArgument("intentional failure"));
}
Ok(NativeResult::Return(Value::Object(value)))
}
}
#[test]
fn scoped_allocations_return_one_root_and_clean_up_errors() {
let mut vm = Vm::new().unwrap();
let function = vm
.allocate(ContextNativeFunction::new(
"allocate",
Allocate(false),
vec![],
))
.unwrap();
let value = vm.call(function, &[]).unwrap();
let Value::Object(object) = value else {
panic!("expected object")
};
assert_eq!(vm.ref_count(object).unwrap(), 1);
assert_eq!(vm.object_count(), 2);
vm.release_value(value).unwrap();
vm.replace_payload(
function,
ContextNativeFunction::new("fail", Allocate(true), vec![]),
)
.unwrap();
assert!(vm.call(function, &[]).is_err());
assert_eq!(vm.object_count(), 1);
vm.release(function).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[derive(Debug)]
struct Delegate(ObjectRef);
impl NativeCallable for Delegate {
fn invoke(&self, _cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
Ok(NativeResult::Invoke {
function: self.0,
arguments: args.to_vec(),
})
}
}
#[test]
fn native_delegation_shares_budget_and_cleans_owned_arguments() {
let mut vm = Vm::new().unwrap();
let target = vm
.allocate(ContextNativeFunction::new(
"target",
Allocate(false),
vec![],
))
.unwrap();
let delegate = vm
.allocate(ContextNativeFunction::new(
"delegate",
Delegate(target),
vec![target],
))
.unwrap();
let arg = vm.new_object().unwrap();
assert!(
vm.call_with_limits(
delegate,
&[Value::Object(arg)],
ExecutionLimits {
max_frames: 1,
max_instructions: 1
}
)
.is_err()
);
assert_eq!(vm.ref_count(arg).unwrap(), 1);
assert_eq!(vm.ref_count(target).unwrap(), 2);
let value = vm.call(delegate, &[]).unwrap();
vm.release_value(value).unwrap();
vm.release(delegate).unwrap();
vm.release(target).unwrap();
vm.release(arg).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn weak_references_expire_without_retaining_reused_slots() {
let mut vm = Vm::new().unwrap();
let object = vm.new_object().unwrap();
let weak = vm.downgrade(object).unwrap();
let upgraded = vm.upgrade(weak).unwrap().unwrap();
vm.release(upgraded).unwrap();
vm.release(object).unwrap();
let replacement = vm.new_object().unwrap();
assert!(vm.upgrade(weak).unwrap().is_none());
let mut foreign = Vm::new().unwrap();
assert_eq!(foreign.upgrade(weak), Err(VmError::ForeignHandle));
vm.release(replacement).unwrap();
}
#[test]
fn changes_coalesce_cover_containers_and_do_not_keep_sources_alive() {
let mut vm = Vm::new().unwrap();
let object = vm.new_object().unwrap();
vm.set_attr(object, "value", Value::Bool(false)).unwrap();
vm.set_attr(object, "value", Value::Bool(true)).unwrap();
let version = vm.change_version(object).unwrap();
let changes = vm.take_changes();
assert!(!changes.rescan_required);
assert_eq!(changes.changes.len(), 1);
assert_eq!(changes.changes[0].key, ChangeKey::Attribute("value".into()));
assert_eq!(changes.changes[0].version, version);
let cell = vm.new_cell(None).unwrap();
vm.cell_set(cell, Value::Object(object)).unwrap();
vm.cell_clear(cell).unwrap();
assert_eq!(vm.take_changes().changes[0].key, ChangeKey::Contents);
vm.set_attr(object, "self", Value::Object(object)).unwrap();
vm.release(object).unwrap();
assert_eq!(vm.collect_cycles(), 1);
vm.release(cell).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn notification_overflow_requires_rescan_instead_of_losing_changes_silently() {
let mut vm = Vm::new().unwrap();
let object = vm.new_object().unwrap();
for index in 0..4100 {
vm.set_attr(object, index.to_string(), Value::Nil).unwrap();
}
let changes = vm.take_changes();
assert!(changes.rescan_required);
assert!(changes.changes.is_empty());
vm.set_attr(object, "after", Value::Nil).unwrap();
assert_eq!(vm.take_changes().changes.len(), 1);
vm.release(object).unwrap();
}
#[derive(Debug)]
struct ReadResult {
source: ObjectRef,
mutate: bool,
}
impl NativeCallable for ReadResult {
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
let value = cx.store().get_attr(self.source, "value")?;
if self.mutate {
cx.set_attr(self.source, "value", Value::Number(99_i64.into()))?;
}
let result = cx.new_object()?;
cx.set_attr(result, "copied", value)?;
Ok(NativeResult::Return(Value::Object(result)))
}
}
#[test]
fn read_only_calls_track_borrowed_dependencies_allow_results_and_restore_after_error() {
let mut vm = Vm::new().unwrap();
let source = vm.new_object().unwrap();
vm.set_attr(source, "value", Value::Number(7_i64.into()))
.unwrap();
let function = vm
.allocate(ContextNativeFunction::new(
"read result",
ReadResult {
source,
mutate: false,
},
vec![source],
))
.unwrap();
vm.take_changes();
let (value, reads) = vm
.call_read_only_with_limits(function, &[], ExecutionLimits::default())
.unwrap();
assert!(reads.contains(&source));
assert!(vm.take_changes().changes.is_empty());
let Value::Object(result) = value else {
panic!("result")
};
assert_eq!(
vm.get_attr(result, "copied").unwrap(),
Value::Number(7_i64.into())
);
vm.release(result).unwrap();
vm.replace_payload(
function,
ContextNativeFunction::new(
"illegal write",
ReadResult {
source,
mutate: true,
},
vec![source],
),
)
.unwrap();
assert!(
vm.call_read_only_with_limits(function, &[], ExecutionLimits::default())
.is_err()
);
assert_eq!(
vm.get_attr(source, "value").unwrap(),
Value::Number(7_i64.into())
);
vm.set_attr(source, "value", Value::Number(8_i64.into()))
.unwrap();
vm.release(function).unwrap();
vm.release(source).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn annotation_reads_remain_dependencies_without_becoming_program_content_reads() {
use endor_core::AnnotationType;
let mut store = objects::ObjectStore::new().unwrap();
let object = store.new_object().unwrap();
store
.set_attr(object, "value", Value::Number(1.into()))
.unwrap();
let scope = store.begin_read_only();
let token = store.begin_read_tracking().unwrap();
assert!(
store
.matches_annotation(&Value::Object(object), AnnotationType::Any)
.unwrap()
);
let reads = store.end_read_tracking_details(token).unwrap();
assert!(
reads
.iter()
.any(|read| read.object == object && !read.content_read)
);
let token = store.begin_read_tracking().unwrap();
assert!(
!store
.matches_annotation(&Value::Object(object), AnnotationType::Number)
.unwrap()
);
let mut foreign = objects::ObjectStore::new().unwrap();
let alien = foreign.new_object().unwrap();
assert!(matches!(
store.matches_annotation(&Value::Object(alien), AnnotationType::Any),
Err(VmError::ForeignHandle)
));
foreign.release(alien).unwrap();
store.get_attr(object, "value").unwrap();
let reads = store.end_read_tracking_details(token).unwrap();
assert!(
reads
.iter()
.any(|read| read.object == object && read.content_read)
);
assert!(store.end_read_only(scope).unwrap().contains(&object));
store.release(object).unwrap();
assert_eq!(store.object_count(), 0);
}