Skip to content

Commit 6bd8334

Browse files
authored
test(vm): add round-trip tests for bytecode argument encode/decode (boa-dev#4913)
# test(vm): add round-trip tests for bytecode argument encode/decode ## Summary Adds unit tests for the `Argument` trait's encode/decode logic in `core/engine/src/vm/opcode/args.rs`. The bytecode argument serialization uses `unsafe` via `read_unchecked` and previously had no test coverage. These tests validate round-trip correctness for all argument types used by VM opcodes. ## Motivation The VM's bytecode format encodes instruction arguments (addresses, register operands, literals, etc.) into a compact byte stream. The `Argument` trait defines `encode` and `decode` for each type, with decoding implemented via a `read` helper that uses `read_unchecked` for unaligned reads from byte slices. This `unsafe` path had no unit tests, leaving encode/decode correctness unverified and vulnerable to regressions from layout changes, endianness assumptions, or buffer-boundary bugs. Adding round-trip tests ensures that any value passed through encode → decode returns the same logical value, and that truncated buffers panic as expected rather than producing undefined behavior. ## Changes | Category | Description | |----------|-------------| | **Added** | `#[cfg(test)] mod tests` with round-trip helpers `round_trip` and `round_trip_eq` | | **Added** | Round-trip tests for `()`, `Address`, `RegisterOperand`, `VaryingOperand` | | **Added** | Round-trip tests for primitives: `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `f32`, `f64` | | **Added** | Round-trip tests for tuples `(u8, u8)`, `(u32, u32)`, `(Address, RegisterOperand)` | | **Added** | Round-trip tests for `ThinVec<u32>` and `ThinVec<RegisterOperand>` | | **Added** | `test_encoded_size_matches_type_size` — verifies encoded byte lengths match `size_of` for `u32`, `u64`, `f64` | | **Added** | `decode_truncated_buffer_panics` — ensures decoding from a too-small buffer panics with "buffer too small" | ## Technical Details - **File modified:** `core/engine/src/vm/opcode/args.rs` - **Lines changed:** +114 (test module only) - **Behavioral impact:** None — tests only; no production code changes The `round_trip` helper encodes a value, decodes from the same buffer, and asserts equality (for types implementing `PartialEq`). The `round_trip_eq` helper accepts a custom comparison function for types like `RegisterOperand` and `VaryingOperand` that do not implement `PartialEq`, comparing via `u32::from()`. Float tests use `0.0` and `1.5` only; `NaN` is excluded because IEEE 754 defines `NaN != NaN`. The panic test uses `#[should_panic(expected = "buffer too small")]` to assert the `read` function's buffer-length check triggers correctly when decoding `u32` from a 2-byte slice. ## Testing - [x] `cargo test -p boa_engine --lib` — all 9 new tests pass - [x] `test_unit_round_trip`, `test_address_round_trip`, `test_register_operand_round_trip`, `test_varying_operand_round_trip` - [x] `test_primitive_round_trips`, `test_tuple_round_trips`, `test_thin_vec_round_trip` - [x] `test_encoded_size_matches_type_size`, `decode_truncated_buffer_panics` - [x] `cargo build` — successful compilation
1 parent 2ebc979 commit 6bd8334

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

core/engine/src/vm/opcode/args.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,129 @@ macro_rules! impl_argument_for_int {
218218
}
219219

220220
impl_argument_for_int!(u8 u16 u32 u64 i8 i16 i32 f32 f64);
221+
222+
#[cfg(test)]
223+
mod tests {
224+
use super::{Address, Argument, RegisterOperand, VaryingOperand};
225+
use std::mem::size_of;
226+
use thin_vec::ThinVec;
227+
228+
fn round_trip<T: Argument + PartialEq + Clone>(value: &T) {
229+
let mut bytes = Vec::new();
230+
value.clone().encode(&mut bytes);
231+
let (decoded, pos) = T::decode(&bytes, 0);
232+
assert_eq!(decoded, *value);
233+
assert_eq!(pos, bytes.len());
234+
}
235+
236+
fn round_trip_eq<T: Argument + Clone, F: Fn(&T, &T) -> bool>(value: &T, eq: F) {
237+
let mut bytes = Vec::new();
238+
value.clone().encode(&mut bytes);
239+
let (decoded, pos) = T::decode(&bytes, 0);
240+
assert!(eq(&decoded, value));
241+
assert_eq!(pos, bytes.len());
242+
}
243+
244+
#[test]
245+
fn test_unit_round_trip() {
246+
round_trip(&());
247+
}
248+
249+
#[test]
250+
fn test_address_round_trip() {
251+
round_trip_eq(&Address::new(0), |a, b| u32::from(*a) == u32::from(*b));
252+
round_trip_eq(&Address::new(0x1234_5678), |a, b| {
253+
u32::from(*a) == u32::from(*b)
254+
});
255+
}
256+
257+
#[test]
258+
fn test_register_operand_round_trip() {
259+
round_trip_eq(&RegisterOperand::new(0), |a, b| {
260+
u32::from(*a) == u32::from(*b)
261+
});
262+
round_trip_eq(&RegisterOperand::new(255), |a, b| {
263+
u32::from(*a) == u32::from(*b)
264+
});
265+
}
266+
267+
#[test]
268+
fn test_varying_operand_round_trip() {
269+
round_trip_eq(&VaryingOperand::new(0), |a, b| {
270+
u32::from(*a) == u32::from(*b)
271+
});
272+
round_trip_eq(&VaryingOperand::new(0xFFFF_FFFF), |a, b| {
273+
u32::from(*a) == u32::from(*b)
274+
});
275+
}
276+
277+
#[test]
278+
fn test_primitive_round_trips() {
279+
round_trip(&0u8);
280+
round_trip(&255u8);
281+
round_trip(&0i8);
282+
round_trip(&(-128i8));
283+
round_trip(&0u16);
284+
round_trip(&0xFFFFu16);
285+
round_trip(&(-32768i16));
286+
round_trip(&0u32);
287+
round_trip(&0xFFFF_FFFFu32);
288+
round_trip(&0i32);
289+
round_trip(&i32::MIN);
290+
round_trip(&0u64);
291+
round_trip(&0xFFFF_FFFF_FFFF_FFFFu64);
292+
round_trip(&0.0f32);
293+
round_trip(&1.5f32);
294+
round_trip(&0.0f64);
295+
round_trip(&1.5f64);
296+
}
297+
298+
#[test]
299+
fn test_tuple_round_trips() {
300+
round_trip(&(0u8, 1u8));
301+
round_trip(&(0u32, 1u32));
302+
let tuple = (Address::new(0), RegisterOperand::new(1));
303+
round_trip_eq(&tuple, |a, b| {
304+
u32::from(a.0) == u32::from(b.0) && u32::from(a.1) == u32::from(b.1)
305+
});
306+
}
307+
308+
#[test]
309+
fn test_thin_vec_round_trip() {
310+
let v: ThinVec<u32> = ThinVec::new();
311+
round_trip(&v);
312+
let v: ThinVec<u32> = [1u32, 2, 3].into_iter().collect();
313+
round_trip(&v);
314+
let v: ThinVec<RegisterOperand> = [RegisterOperand::new(0), RegisterOperand::new(1)]
315+
.into_iter()
316+
.collect();
317+
round_trip_eq(&v, |a, b| {
318+
a.len() == b.len()
319+
&& a.iter()
320+
.zip(b.iter())
321+
.all(|(x, y)| u32::from(*x) == u32::from(*y))
322+
});
323+
}
324+
325+
#[test]
326+
fn test_encoded_size_matches_type_size() {
327+
let mut bytes = Vec::new();
328+
Address::new(0xDEAD_BEEF).encode(&mut bytes);
329+
assert_eq!(bytes.len(), size_of::<u32>());
330+
331+
bytes.clear();
332+
(0u64).encode(&mut bytes);
333+
assert_eq!(bytes.len(), size_of::<u64>());
334+
335+
bytes.clear();
336+
(0.0f64).encode(&mut bytes);
337+
assert_eq!(bytes.len(), size_of::<f64>());
338+
}
339+
340+
#[test]
341+
#[should_panic(expected = "buffer too small")]
342+
fn decode_truncated_buffer_panics() {
343+
let bytes = [0u8; 2];
344+
let _ = u32::decode(&bytes, 0);
345+
}
346+
}

0 commit comments

Comments
 (0)