Skip to content

Commit 8181487

Browse files
Haofeiclaude
andcommitted
test: cover VM<->native value marshalling (value_convert)
Adds in-crate unit tests for native_value_from_vm_value / vm_value_from_native_value / vm_map_key_from_native_value: every variant both directions, the Option bridge, managed-unwrap, the closure rejection, and the map-key string fallback. The host boundary was ~52% covered; a missed arm here is a silently wrong value at a native call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3a24c28 commit 8181487

1 file changed

Lines changed: 225 additions & 0 deletions

File tree

crates/rsscript/src/reg_vm/value_convert.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,228 @@ pub(super) fn vm_map_key_from_native_value(value: NativeValue) -> VmMapKey {
653653
other => VmMapKey::String(Rc::new(format!("{other:?}"))),
654654
}
655655
}
656+
657+
#[cfg(test)]
658+
mod tests {
659+
//! VM <-> native value marshalling: every variant in both directions, the
660+
//! `Option` bridge, managed-unwrap, the closure rejection, and the map-key
661+
//! fallback. This is the host boundary, so a missed arm here is a silently
662+
//! wrong value at a native call.
663+
use super::*;
664+
use crate::vm_value::{VmClosure, VmNative};
665+
use std::collections::{BTreeMap, VecDeque};
666+
667+
fn to_native(value: VmValue) -> NativeValue {
668+
native_value_from_vm_value(value).expect("conversion should succeed")
669+
}
670+
671+
#[test]
672+
fn vm_to_native_scalars() {
673+
assert_eq!(to_native(VmValue::Unit), NativeValue::Unit);
674+
assert_eq!(to_native(VmValue::Int(7)), NativeValue::Int(7));
675+
assert_eq!(to_native(VmValue::Float(1.5)), NativeValue::Float(1.5));
676+
assert_eq!(to_native(VmValue::Bool(true)), NativeValue::Bool(true));
677+
assert_eq!(to_native(VmValue::Char('x')), NativeValue::Char('x'));
678+
assert_eq!(
679+
to_native(VmValue::string("hi")),
680+
NativeValue::String("hi".to_string())
681+
);
682+
assert_eq!(
683+
to_native(VmValue::Bytes(Rc::new(vec![1, 2, 3]))),
684+
NativeValue::Bytes(vec![1, 2, 3])
685+
);
686+
assert_eq!(
687+
to_native(VmValue::Json(Rc::new(serde_json::json!({"a": 1})))),
688+
NativeValue::Json(serde_json::json!({"a": 1}))
689+
);
690+
}
691+
692+
#[test]
693+
fn vm_to_native_collections() {
694+
let list = VmValue::List(Rc::new(RefCell::new(vec![
695+
VmValue::Int(1),
696+
VmValue::Int(2),
697+
])));
698+
assert_eq!(
699+
to_native(list),
700+
NativeValue::List(vec![NativeValue::Int(1), NativeValue::Int(2)])
701+
);
702+
703+
// `Deque` also marshals to a native list.
704+
let deque = VmValue::Deque(Rc::new(RefCell::new(VecDeque::from(vec![VmValue::Int(9)]))));
705+
assert_eq!(
706+
to_native(deque),
707+
NativeValue::List(vec![NativeValue::Int(9)])
708+
);
709+
710+
let mut map: ValueMap = ValueMap::default();
711+
map.insert(VmMapKey::Int(1), VmValue::string("one"));
712+
assert_eq!(
713+
to_native(VmValue::Map(Rc::new(RefCell::new(map)))),
714+
NativeValue::Map(vec![(
715+
NativeValue::Int(1),
716+
NativeValue::String("one".to_string())
717+
)])
718+
);
719+
}
720+
721+
#[test]
722+
fn vm_to_native_struct_variant_native() {
723+
let s = VmValue::Struct(Rc::new(VmStruct::from_named(
724+
"Point",
725+
[("x", VmValue::Int(1)), ("y", VmValue::Int(2))],
726+
)));
727+
match to_native(s) {
728+
NativeValue::Struct { name, fields } => {
729+
assert_eq!(name, "Point");
730+
assert_eq!(fields["x"], NativeValue::Int(1));
731+
assert_eq!(fields["y"], NativeValue::Int(2));
732+
}
733+
other => panic!("expected struct, got {other:?}"),
734+
}
735+
736+
let v = VmValue::Variant(Rc::new(VmStruct::from_named(
737+
"Tag",
738+
[("v", VmValue::Bool(true))],
739+
)));
740+
match to_native(v) {
741+
NativeValue::Variant { name, fields } => {
742+
assert_eq!(name, "Tag");
743+
assert_eq!(fields["v"], NativeValue::Bool(true));
744+
}
745+
other => panic!("expected variant, got {other:?}"),
746+
}
747+
748+
let native = VmValue::Native(Rc::new(VmNative {
749+
type_name: Rc::from("Handle"),
750+
id: 42,
751+
}));
752+
assert_eq!(
753+
to_native(native),
754+
NativeValue::Native {
755+
type_name: "Handle".to_string(),
756+
id: 42
757+
}
758+
);
759+
}
760+
761+
#[test]
762+
fn vm_to_native_bridges_option() {
763+
match to_native(VmValue::OptionSome(Box::new(VmValue::Int(5)))) {
764+
NativeValue::Variant { name, fields } => {
765+
assert_eq!(name, "Some");
766+
assert_eq!(fields["value"], NativeValue::Int(5));
767+
}
768+
other => panic!("expected Some variant, got {other:?}"),
769+
}
770+
match to_native(VmValue::OptionNone) {
771+
NativeValue::Variant { name, fields } => {
772+
assert_eq!(name, "None");
773+
assert!(fields.is_empty());
774+
}
775+
other => panic!("expected None variant, got {other:?}"),
776+
}
777+
}
778+
779+
#[test]
780+
fn vm_to_native_unwraps_managed() {
781+
let managed = VmValue::Managed(Rc::new(RefCell::new(VmValue::Int(11))));
782+
assert_eq!(to_native(managed), NativeValue::Int(11));
783+
}
784+
785+
#[test]
786+
fn vm_to_native_rejects_closure() {
787+
let closure = VmValue::Closure(Rc::new(VmClosure {
788+
function: 0,
789+
captures: Vec::new(),
790+
}));
791+
match native_value_from_vm_value(closure) {
792+
Err(EvalError::Runtime(message)) => assert!(message.contains("Closure"), "{message}"),
793+
other => panic!("expected closure rejection, got {other:?}"),
794+
}
795+
}
796+
797+
#[test]
798+
fn native_to_vm_round_trips_every_variant() {
799+
// A native list carrying one of each variant; round-tripping the whole
800+
// thing exercises every arm of `vm_value_from_native_value` and the
801+
// matching arm of `native_value_from_vm_value`. Single-entry map keeps
802+
// ordering stable across the HashMap hop.
803+
let mut struct_fields = BTreeMap::new();
804+
struct_fields.insert("a".to_string(), NativeValue::Int(1));
805+
let mut variant_fields = BTreeMap::new();
806+
variant_fields.insert("b".to_string(), NativeValue::Bool(true));
807+
808+
let native = NativeValue::List(vec![
809+
NativeValue::Unit,
810+
NativeValue::Int(1),
811+
NativeValue::Float(2.5),
812+
NativeValue::Bool(false),
813+
NativeValue::Char('q'),
814+
NativeValue::String("s".to_string()),
815+
NativeValue::Bytes(vec![7, 8]),
816+
NativeValue::Json(serde_json::json!([1, 2])),
817+
NativeValue::Map(vec![(
818+
NativeValue::Int(3),
819+
NativeValue::String("v".to_string()),
820+
)]),
821+
NativeValue::Struct {
822+
name: "S".to_string(),
823+
fields: struct_fields,
824+
},
825+
NativeValue::Variant {
826+
name: "Custom".to_string(),
827+
fields: variant_fields,
828+
},
829+
NativeValue::Native {
830+
type_name: "H".to_string(),
831+
id: 5,
832+
},
833+
]);
834+
835+
let back = native_value_from_vm_value(vm_value_from_native_value(native.clone()))
836+
.expect("round trip should succeed");
837+
assert_eq!(native, back);
838+
}
839+
840+
#[test]
841+
fn native_to_vm_bridges_option_variants() {
842+
let mut some_fields = BTreeMap::new();
843+
some_fields.insert("value".to_string(), NativeValue::Int(8));
844+
assert!(matches!(
845+
vm_value_from_native_value(NativeValue::Variant {
846+
name: "Some".to_string(),
847+
fields: some_fields,
848+
}),
849+
VmValue::OptionSome(_)
850+
));
851+
assert!(matches!(
852+
vm_value_from_native_value(NativeValue::Variant {
853+
name: "None".to_string(),
854+
fields: BTreeMap::new(),
855+
}),
856+
VmValue::OptionNone
857+
));
858+
}
859+
860+
#[test]
861+
fn map_key_from_native_falls_back_to_string() {
862+
assert_eq!(
863+
vm_map_key_from_native_value(NativeValue::Bool(true)),
864+
VmMapKey::Bool(true)
865+
);
866+
assert_eq!(
867+
vm_map_key_from_native_value(NativeValue::Int(2)),
868+
VmMapKey::Int(2)
869+
);
870+
assert_eq!(
871+
vm_map_key_from_native_value(NativeValue::String("k".to_string())),
872+
VmMapKey::String(Rc::new("k".to_string()))
873+
);
874+
// A non-scalar key has no VM key form, so it falls back to its debug string.
875+
match vm_map_key_from_native_value(NativeValue::Float(1.0)) {
876+
VmMapKey::String(s) => assert!(s.contains("Float"), "{s}"),
877+
other => panic!("expected string fallback, got {other:?}"),
878+
}
879+
}
880+
}

0 commit comments

Comments
 (0)