test: 添加 JsValue 测试验证并修复类型冲突问题

This commit is contained in:
2026-06-05 02:10:35 +08:00
parent f34f10bcf2
commit d42e65949d
2 changed files with 312 additions and 5 deletions
+106 -5
View File
@@ -37,6 +37,11 @@ fn get_tag(bits: u64) -> u64 {
pub struct JsValue(u64); pub struct JsValue(u64);
impl JsValue { impl JsValue {
#[allow(dead_code)]
pub(crate) fn from_bits(bits: u64) -> Self {
Self(bits)
}
pub fn int(v: i32) -> Self { pub fn int(v: i32) -> Self {
Self(make_tag(TAG_INT) | (v as u32 as u64)) Self(make_tag(TAG_INT) | (v as u32 as u64))
} }
@@ -44,7 +49,7 @@ impl JsValue {
pub fn float(v: f64) -> Self { pub fn float(v: f64) -> Self {
let bits = v.to_bits(); let bits = v.to_bits();
if is_nan_bits(bits) { if is_nan_bits(bits) {
Self(QNAN_PREFIX | 1) Self(QNAN_PREFIX | (7u64 << TAG_SHIFT) | 1)
} else { } else {
Self(bits) Self(bits)
} }
@@ -69,15 +74,15 @@ impl JsValue {
} }
pub fn is_double(&self) -> bool { pub fn is_double(&self) -> bool {
!is_nan_bits(self.0) !is_nan_boxed(self.0)
} }
pub fn is_int(&self) -> bool { 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 { 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 { pub fn is_null(&self) -> bool {
@@ -89,7 +94,7 @@ impl JsValue {
} }
pub fn is_object(&self) -> bool { 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 { 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 (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 { impl fmt::Display for JsValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_int() { 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::<f64>(), |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::<bool>(), |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::<u64>(), |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::<f64>(), |v| {
let a = JsValue::float(v);
let b = JsValue::float(a.as_double());
assert_eq!(a, b);
Ok(())
})
.unwrap();
}
}
+206
View File
@@ -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());
}