From d42e65949d5851d2940fb95975f3aefaedd5717f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 5 Jun 2026 02:10:35 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20JsValue=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=AA=8C=E8=AF=81=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=86=B2=E7=AA=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/value.rs | 111 ++++++++++++++- crates/oxide_vm/tests/value_tests.rs | 206 +++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 crates/oxide_vm/tests/value_tests.rs diff --git a/crates/oxide_vm/src/value.rs b/crates/oxide_vm/src/value.rs index 006da3a8..e0e1d6ce 100644 --- a/crates/oxide_vm/src/value.rs +++ b/crates/oxide_vm/src/value.rs @@ -37,6 +37,11 @@ fn get_tag(bits: u64) -> u64 { pub struct JsValue(u64); impl JsValue { + #[allow(dead_code)] + pub(crate) fn from_bits(bits: u64) -> Self { + Self(bits) + } + pub fn int(v: i32) -> Self { Self(make_tag(TAG_INT) | (v as u32 as u64)) } @@ -44,7 +49,7 @@ impl JsValue { pub fn float(v: f64) -> Self { let bits = v.to_bits(); if is_nan_bits(bits) { - Self(QNAN_PREFIX | 1) + Self(QNAN_PREFIX | (7u64 << TAG_SHIFT) | 1) } else { Self(bits) } @@ -69,15 +74,15 @@ impl JsValue { } pub fn is_double(&self) -> bool { - !is_nan_bits(self.0) + !is_nan_boxed(self.0) } pub fn is_int(&self) -> bool { - get_tag(self.0) == TAG_INT + is_nan_boxed(self.0) && get_tag(self.0) == TAG_INT } pub fn is_bool(&self) -> bool { - get_tag(self.0) == TAG_BOOL + is_nan_boxed(self.0) && get_tag(self.0) == TAG_BOOL } pub fn is_null(&self) -> bool { @@ -89,7 +94,7 @@ impl JsValue { } pub fn is_object(&self) -> bool { - get_tag(self.0) == TAG_OBJECT + is_nan_boxed(self.0) && get_tag(self.0) == TAG_OBJECT } pub fn as_double(&self) -> f64 { @@ -117,6 +122,10 @@ fn is_nan_bits(bits: u64) -> bool { (bits & EXP_MASK) == EXP_MASK && (bits & MANTISSA_MASK) != 0 } +fn is_nan_boxed(bits: u64) -> bool { + (0xFFF8..=0xFFFC).contains(&((bits >> 48) as u16)) +} + impl fmt::Display for JsValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_int() { @@ -172,3 +181,95 @@ impl fmt::Debug for JsValue { } } } + +#[cfg(test)] +mod tests { + use super::JsValue; + use proptest::prelude::*; + use proptest::test_runner::TestRunner; + + #[test] + fn int_roundtrip_prop() { + let mut runner = TestRunner::default(); + runner + .run(&(i32::MIN..=i32::MAX), |v| { + let val = JsValue::int(v); + assert!(val.is_int()); + assert_eq!(val.as_int(), v); + Ok(()) + }) + .unwrap(); + } + + #[test] + fn float_roundtrip_prop() { + let mut runner = TestRunner::default(); + runner + .run(&any::(), |v| { + let val = JsValue::float(v); + assert!(val.is_double()); + if !v.is_nan() { + assert_eq!(val.as_double(), v); + } + Ok(()) + }) + .unwrap(); + } + + #[test] + fn bool_roundtrip_prop() { + let mut runner = TestRunner::default(); + runner + .run(&any::(), |v| { + let val = JsValue::bool(v); + assert!(val.is_bool()); + assert_eq!(val.as_bool(), v); + Ok(()) + }) + .unwrap(); + } + + #[test] + fn random_u64_type_safety() { + let mut runner = TestRunner::default(); + runner + .run(&any::(), |bits| { + let val = JsValue::from_bits(bits); + let matched = [ + val.is_double(), + val.is_int(), + val.is_bool(), + val.is_null(), + val.is_undefined(), + val.is_object(), + ]; + let count = matched.iter().filter(|&&x| x).count(); + assert_eq!(count, 1, "bits={bits:#018x} matched {count} types"); + Ok(()) + }) + .unwrap(); + } + + #[test] + fn null_identity() { + assert_eq!(JsValue::null(), JsValue::null()); + } + + #[test] + fn undefined_identity() { + assert_eq!(JsValue::undefined(), JsValue::undefined()); + } + + #[test] + fn canonicalization_idempotent() { + let mut runner = TestRunner::default(); + runner + .run(&any::(), |v| { + let a = JsValue::float(v); + let b = JsValue::float(a.as_double()); + assert_eq!(a, b); + Ok(()) + }) + .unwrap(); + } +} diff --git a/crates/oxide_vm/tests/value_tests.rs b/crates/oxide_vm/tests/value_tests.rs new file mode 100644 index 00000000..91ade956 --- /dev/null +++ b/crates/oxide_vm/tests/value_tests.rs @@ -0,0 +1,206 @@ +use oxide_vm::JsValue; + +#[test] +fn int_roundtrip() { + for v in [0, 1, -1, 42, i32::MIN, i32::MAX] { + let val = JsValue::int(v); + assert!(val.is_int(), "int({v}) should be int"); + assert_eq!(val.as_int(), v, "int({v}) roundtrip failed"); + } +} + +#[test] +fn float_roundtrip() { + for v in [0.0, -0.0, 1.0, -1.0, 42.5, f64::MIN, f64::MAX] { + let val = JsValue::float(v); + assert!(val.is_double(), "float({v}) should be double"); + assert_eq!(val.as_double(), v, "float({v}) roundtrip failed"); + } +} + +#[test] +fn float_infinity() { + let val = JsValue::float(f64::INFINITY); + assert!(val.is_double()); + assert!(val.as_double().is_infinite()); + assert!(val.as_double().is_sign_positive()); + + let val = JsValue::float(f64::NEG_INFINITY); + assert!(val.is_double()); + assert!(val.as_double().is_infinite()); + assert!(val.as_double().is_sign_negative()); +} + +#[test] +fn float_nan_canonicalization() { + let val = JsValue::float(f64::NAN); + assert!(val.is_double(), "NaN should be classified as double"); + let d = val.as_double(); + assert!(d.is_nan(), "NaN value should still be NaN"); +} + +#[test] +fn bool_roundtrip() { + let t = JsValue::bool(true); + assert!(t.is_bool()); + assert!(t.as_bool()); + + let f = JsValue::bool(false); + assert!(f.is_bool()); + assert!(!f.as_bool()); +} + +#[test] +fn null_type_check() { + let val = JsValue::null(); + assert!(val.is_null()); + assert!(!val.is_int()); + assert!(!val.is_double()); + assert!(!val.is_bool()); + assert!(!val.is_undefined()); + assert!(!val.is_object()); +} + +#[test] +fn undefined_type_check() { + let val = JsValue::undefined(); + assert!(val.is_undefined()); + assert!(!val.is_int()); + assert!(!val.is_double()); + assert!(!val.is_bool()); + assert!(!val.is_null()); + assert!(!val.is_object()); +} + +#[test] +fn null_equals_null() { + assert_eq!(JsValue::null(), JsValue::null()); +} + +#[test] +fn null_not_equals_undefined() { + assert_ne!(JsValue::null(), JsValue::undefined()); +} + +#[test] +fn object_pointer_roundtrip() { + let x = 42u8; + let val = JsValue::object(&x as *const u8); + assert!(val.is_object()); + let ptr = val.as_ptr(); + assert_eq!(ptr, &x as *const u8); +} + +#[test] +fn display_int() { + assert_eq!(format!("{}", JsValue::int(42)), "42"); + assert_eq!(format!("{}", JsValue::int(-1)), "-1"); +} + +#[test] +fn display_double() { + let val = JsValue::float(42.5); + assert!(format!("{}", val).contains("42.5")); +} + +#[test] +fn display_bool() { + assert_eq!(format!("{}", JsValue::bool(true)), "true"); + assert_eq!(format!("{}", JsValue::bool(false)), "false"); +} + +#[test] +fn display_null() { + assert_eq!(format!("{}", JsValue::null()), "null"); +} + +#[test] +fn display_undefined() { + assert_eq!(format!("{}", JsValue::undefined()), "undefined"); +} + +#[test] +fn display_nan() { + assert_eq!(format!("{}", JsValue::float(f64::NAN)), "NaN"); +} + +#[test] +fn display_infinity() { + assert_eq!(format!("{}", JsValue::float(f64::INFINITY)), "Infinity"); +} + +#[test] +fn debug_int() { + assert_eq!(format!("{:?}", JsValue::int(42)), "JsValue(Int(42))"); +} + +#[test] +fn debug_double() { + let val = JsValue::float(42.5); + assert!(format!("{:?}", val).starts_with("JsValue(Double(")); +} + +#[test] +fn debug_bool() { + assert_eq!(format!("{:?}", JsValue::bool(true)), "JsValue(Bool(true))"); +} + +#[test] +fn debug_null() { + assert_eq!(format!("{:?}", JsValue::null()), "JsValue(Null)"); +} + +#[test] +fn debug_undefined() { + assert_eq!(format!("{:?}", JsValue::undefined()), "JsValue(Undefined)"); +} + +#[test] +fn debug_object() { + let x = 0u8; + let val = JsValue::object(&x as *const u8); + assert!(format!("{:?}", val).starts_with("JsValue(Object(")); +} + +#[test] +fn copy_semantics() { + let a = JsValue::int(42); + let b = a; + assert_eq!(a, b); + assert_eq!(a.as_int(), 42); + assert_eq!(b.as_int(), 42); +} + +#[test] +fn type_exclusivity_int() { + let val = JsValue::int(1); + assert!(val.is_int()); + assert!(!val.is_double()); + assert!(!val.is_bool()); + assert!(!val.is_null()); + assert!(!val.is_undefined()); + assert!(!val.is_object()); +} + +#[test] +fn type_exclusivity_double() { + let val = JsValue::float(0.0); + assert!(val.is_double()); + assert!(!val.is_int()); + assert!(!val.is_bool()); + assert!(!val.is_null()); + assert!(!val.is_undefined()); + assert!(!val.is_object()); +} + +#[test] +fn type_exclusivity_object() { + let x = 0u8; + let val = JsValue::object(&x as *const u8); + assert!(val.is_object()); + assert!(!val.is_int()); + assert!(!val.is_double()); + assert!(!val.is_bool()); + assert!(!val.is_null()); + assert!(!val.is_undefined()); +}