1005 lines
36 KiB
Rust
1005 lines
36 KiB
Rust
use super::*;
|
|
use crate::fs::{FsBackend, FsEntry, FsError};
|
|
use prometeu_bytecode::TRAP_TYPE;
|
|
use prometeu_bytecode::Value;
|
|
use prometeu_bytecode::assembler::assemble;
|
|
use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl};
|
|
use prometeu_drivers::hardware::Hardware;
|
|
use prometeu_hal::AudioOpStatus;
|
|
use prometeu_hal::GfxOpStatus;
|
|
use prometeu_hal::InputSignals;
|
|
use prometeu_hal::asset::{
|
|
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
|
|
};
|
|
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
|
use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
|
use prometeu_hal::syscalls::caps;
|
|
use prometeu_vm::VmInitError;
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
#[derive(Default)]
|
|
struct MemFsBackend {
|
|
files: HashMap<String, Vec<u8>>,
|
|
}
|
|
|
|
impl FsBackend for MemFsBackend {
|
|
fn mount(&mut self) -> Result<(), FsError> {
|
|
Ok(())
|
|
}
|
|
|
|
fn unmount(&mut self) {}
|
|
|
|
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
|
|
Ok(Vec::new())
|
|
}
|
|
|
|
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
|
|
self.files.get(path).cloned().ok_or(FsError::NotFound)
|
|
}
|
|
|
|
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
|
|
self.files.insert(path.to_string(), data.to_vec());
|
|
Ok(())
|
|
}
|
|
|
|
fn delete(&mut self, path: &str) -> Result<(), FsError> {
|
|
self.files.remove(path).map(|_| ()).ok_or(FsError::NotFound)
|
|
}
|
|
|
|
fn exists(&self, path: &str) -> bool {
|
|
self.files.contains_key(path)
|
|
}
|
|
|
|
fn is_healthy(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge {
|
|
Cartridge {
|
|
app_id: 42,
|
|
title: "Test Cart".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode: AppMode::Game,
|
|
capabilities,
|
|
program,
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
}
|
|
}
|
|
|
|
fn serialized_single_function_module(code: Vec<u8>, syscalls: Vec<SyscallDecl>) -> Vec<u8> {
|
|
serialized_single_function_module_with_consts(code, vec![], syscalls)
|
|
}
|
|
|
|
fn serialized_single_function_module_with_consts(
|
|
code: Vec<u8>,
|
|
const_pool: Vec<ConstantPoolEntry>,
|
|
syscalls: Vec<SyscallDecl>,
|
|
) -> Vec<u8> {
|
|
BytecodeModule {
|
|
version: 0,
|
|
const_pool,
|
|
functions: vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
}],
|
|
code,
|
|
debug_info: None,
|
|
exports: vec![],
|
|
syscalls,
|
|
}
|
|
.serialize()
|
|
}
|
|
|
|
fn test_glyph_payload_size(width: usize, height: usize) -> usize {
|
|
(width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
|
}
|
|
|
|
fn test_glyph_decoded_size(width: usize, height: usize) -> usize {
|
|
width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
|
}
|
|
|
|
fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
|
|
AssetEntry {
|
|
asset_id: 7,
|
|
asset_name: asset_name.to_string(),
|
|
bank_type: BankType::GLYPH,
|
|
offset: 0,
|
|
size: data_len as u64,
|
|
decoded_size: test_glyph_decoded_size(16, 16) as u64,
|
|
codec: AssetCodec::None,
|
|
metadata: serde_json::json!({
|
|
"tile_size": 16,
|
|
"width": 16,
|
|
"height": 16,
|
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn test_glyph_asset_data() -> Vec<u8> {
|
|
let mut data =
|
|
vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2)];
|
|
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2]);
|
|
data
|
|
}
|
|
|
|
#[test]
|
|
fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::NONE);
|
|
|
|
let res = runtime.initialize_vm(&mut vm, &cartridge);
|
|
|
|
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
|
|
assert_eq!(runtime.current_app_id, 0);
|
|
assert_eq!(vm.pc(), 0);
|
|
assert_eq!(vm.operand_stack_top(1), Vec::<Value>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
let res = runtime.initialize_vm(&mut vm, &cartridge);
|
|
|
|
assert!(res.is_ok());
|
|
assert_eq!(runtime.current_app_id, 42);
|
|
assert!(!vm.is_halted());
|
|
}
|
|
|
|
#[test]
|
|
fn tick_returns_error_when_vm_ends_slice_with_trap() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
|
|
let report =
|
|
runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error");
|
|
|
|
match report {
|
|
CrashReport::VmTrap { trap } => {
|
|
assert_eq!(trap.code, TRAP_TYPE);
|
|
assert!(trap.message.contains("Expected integer at index 0"));
|
|
}
|
|
other => panic!("expected VmTrap crash report, got {:?}", other),
|
|
}
|
|
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn tick_returns_panic_report_distinct_from_trap() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]);
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
|
|
let report = runtime
|
|
.tick(&mut vm, &signals, &mut hardware)
|
|
.expect("panic must surface as runtime error");
|
|
|
|
match report {
|
|
CrashReport::VmPanic { message, pc } => {
|
|
assert!(message.contains("HOSTCALL 0 reached execution without loader patching"));
|
|
assert!(pc.is_some());
|
|
}
|
|
other => panic!("expected VmPanic crash report, got {:?}", other),
|
|
}
|
|
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn initialize_vm_success_clears_previous_crash_report() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) });
|
|
|
|
let mut vm = VirtualMachine::default();
|
|
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
assert!(runtime.last_crash_report.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn reset_clears_cartridge_scoped_runtime_state() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
|
|
runtime.tick_index = 77;
|
|
runtime.logical_frame_index = 12;
|
|
runtime.logical_frame_active = true;
|
|
runtime.logical_frame_remaining_cycles = 345;
|
|
runtime.last_frame_cpu_time_us = 678;
|
|
runtime.fs_state = FsState::Mounted;
|
|
runtime.open_files.insert(7, "/save.dat".into());
|
|
runtime.next_handle = 9;
|
|
runtime.current_app_id = 42;
|
|
runtime.current_cartridge_title = "Cart".into();
|
|
runtime.current_cartridge_app_version = "1.2.3".into();
|
|
runtime.current_cartridge_app_mode = AppMode::System;
|
|
runtime.logs_written_this_frame.insert(42, 3);
|
|
runtime.atomic_telemetry.logs_count.store(5, Ordering::Relaxed);
|
|
runtime.atomic_telemetry.current_logs_count.store(8, Ordering::Relaxed);
|
|
runtime.atomic_telemetry.frame_index.store(8, Ordering::Relaxed);
|
|
runtime.atomic_telemetry.cycles_used.store(99, Ordering::Relaxed);
|
|
runtime.atomic_telemetry.completed_logical_frames.store(2, Ordering::Relaxed);
|
|
runtime.last_crash_report =
|
|
Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) });
|
|
runtime.paused = true;
|
|
runtime.debug_step_request = true;
|
|
runtime.needs_prepare_entry_call = true;
|
|
|
|
runtime.reset(&mut vm);
|
|
|
|
assert_eq!(runtime.tick_index, 77);
|
|
assert_eq!(runtime.fs_state, FsState::Mounted);
|
|
assert_eq!(runtime.logical_frame_index, 0);
|
|
assert!(!runtime.logical_frame_active);
|
|
assert_eq!(runtime.logical_frame_remaining_cycles, 0);
|
|
assert_eq!(runtime.last_frame_cpu_time_us, 0);
|
|
assert!(runtime.open_files.is_empty());
|
|
assert_eq!(runtime.next_handle, 1);
|
|
assert_eq!(runtime.current_app_id, 0);
|
|
assert!(runtime.current_cartridge_title.is_empty());
|
|
assert!(runtime.current_cartridge_app_version.is_empty());
|
|
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
|
assert!(runtime.logs_written_this_frame.is_empty());
|
|
assert_eq!(runtime.atomic_telemetry.logs_count.load(Ordering::Relaxed), 0);
|
|
assert_eq!(runtime.atomic_telemetry.current_logs_count.load(Ordering::Relaxed), 0);
|
|
assert_eq!(runtime.atomic_telemetry.frame_index.load(Ordering::Relaxed), 0);
|
|
assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0);
|
|
assert_eq!(runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed), 0);
|
|
assert!(runtime.last_crash_report.is_none());
|
|
assert!(!runtime.paused);
|
|
assert!(!runtime.debug_step_request);
|
|
assert!(!runtime.needs_prepare_entry_call);
|
|
assert_eq!(vm.pc(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
|
|
let good_program = serialized_single_function_module(
|
|
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let good_cartridge = cartridge_with_program(good_program, caps::GFX);
|
|
runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize");
|
|
|
|
runtime.open_files.insert(5, "/save.dat".into());
|
|
runtime.next_handle = 6;
|
|
runtime.paused = true;
|
|
runtime.debug_step_request = true;
|
|
runtime.atomic_telemetry.cycles_used.store(123, Ordering::Relaxed);
|
|
|
|
let bad_program = serialized_single_function_module(
|
|
assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"),
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "clear".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 0,
|
|
}],
|
|
);
|
|
let bad_cartridge = cartridge_with_program(bad_program, caps::NONE);
|
|
|
|
let res = runtime.initialize_vm(&mut vm, &bad_cartridge);
|
|
|
|
assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) })));
|
|
assert_eq!(runtime.current_app_id, 0);
|
|
assert!(runtime.current_cartridge_title.is_empty());
|
|
assert!(runtime.current_cartridge_app_version.is_empty());
|
|
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
|
assert!(runtime.open_files.is_empty());
|
|
assert_eq!(runtime.next_handle, 1);
|
|
assert!(!runtime.paused);
|
|
assert!(!runtime.debug_step_request);
|
|
assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0);
|
|
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "set_sprite".into(),
|
|
version: 1,
|
|
arg_slots: 10,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "operational error must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 512\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "set_sprite".into(),
|
|
version: 1,
|
|
arg_slots: 10,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "invalid sprite index must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "set_sprite".into(),
|
|
version: 1,
|
|
arg_slots: 10,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
let asset_data = test_glyph_asset_data();
|
|
|
|
hardware.assets.initialize_for_cartridge(
|
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
|
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
|
|
AssetsPayloadSource::from_bytes(asset_data),
|
|
);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "invalid gfx parameter range must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_audio_play_sample_operational_error_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "audio".into(),
|
|
name: "play_sample".into(),
|
|
version: 1,
|
|
arg_slots: 5,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "operational error must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_audio_play_voice_invalid_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "audio".into(),
|
|
name: "play".into(),
|
|
version: 1,
|
|
arg_slots: 7,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "invalid voice must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_audio_play_missing_asset_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "audio".into(),
|
|
name: "play".into(),
|
|
version: 1,
|
|
arg_slots: 7,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "missing audio asset must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::BankInvalid as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "audio".into(),
|
|
name: "play".into(),
|
|
version: 1,
|
|
arg_slots: 7,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::AUDIO);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report =
|
|
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap");
|
|
match report {
|
|
CrashReport::VmTrap { trap } => {
|
|
assert_eq!(trap.code, TRAP_TYPE);
|
|
assert!(trap.message.contains("Expected integer at index 0"));
|
|
}
|
|
other => panic!("expected VmTrap crash report, got {:?}", other),
|
|
}
|
|
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_commit_operational_error_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "commit".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "operational error must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_load_missing_asset_returns_status_and_zero_handle() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "load".into(),
|
|
version: 1,
|
|
arg_slots: 2,
|
|
ret_slots: 2,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "missing asset must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(
|
|
vm.operand_stack_top(2),
|
|
vec![Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let asset_data = test_glyph_asset_data();
|
|
hardware.assets.initialize_for_cartridge(
|
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
|
vec![],
|
|
AssetsPayloadSource::from_bytes(asset_data),
|
|
);
|
|
let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "load".into(),
|
|
version: 1,
|
|
arg_slots: 2,
|
|
ret_slots: 2,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "invalid slot must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(
|
|
vm.operand_stack_top(2),
|
|
vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotIndexInvalid as i64)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_status_unknown_handle_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "status".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "unknown asset handle must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT")
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![
|
|
SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "cancel".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
},
|
|
SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "commit".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
},
|
|
],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
let asset_data = test_glyph_asset_data();
|
|
hardware.assets.initialize_for_cartridge(
|
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
|
vec![],
|
|
AssetsPayloadSource::from_bytes(asset_data),
|
|
);
|
|
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "invalid transition must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(handle, 1);
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_cancel_unknown_handle_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "cancel".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "unknown handle cancel must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "cancel".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
|
|
|
let asset_data = test_glyph_asset_data();
|
|
hardware.assets.initialize_for_cartridge(
|
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
|
vec![],
|
|
AssetsPayloadSource::from_bytes(asset_data),
|
|
);
|
|
let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated");
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
|
|
loop {
|
|
match hardware.assets.status(handle) {
|
|
LoadStatus::READY => break,
|
|
LoadStatus::PENDING | LoadStatus::LOADING => {
|
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
}
|
|
other => panic!("unexpected asset status before commit: {:?}", other),
|
|
}
|
|
}
|
|
|
|
assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok);
|
|
hardware.assets.apply_commits();
|
|
assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED);
|
|
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "cancel after commit must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
|
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
|
|
PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\
|
|
HALT"
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![
|
|
SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "set_sprite".into(),
|
|
version: 1,
|
|
arg_slots: 10,
|
|
ret_slots: 1,
|
|
},
|
|
SyscallDecl {
|
|
module: "audio".into(),
|
|
name: "play".into(),
|
|
version: 1,
|
|
arg_slots: 7,
|
|
ret_slots: 1,
|
|
},
|
|
SyscallDecl {
|
|
module: "asset".into(),
|
|
name: "load".into(),
|
|
version: 1,
|
|
arg_slots: 2,
|
|
ret_slots: 2,
|
|
},
|
|
],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "mixed status-first surface must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(
|
|
vm.operand_stack_top(4),
|
|
vec![
|
|
Value::Int64(0),
|
|
Value::Int64(AssetLoadError::AssetNotFound as i64),
|
|
Value::Int64(AudioOpStatus::BankInvalid as i64),
|
|
Value::Int64(GfxOpStatus::BankInvalid as i64),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "gfx".into(),
|
|
name: "set_sprite".into(),
|
|
version: 1,
|
|
arg_slots: 10,
|
|
ret_slots: 1,
|
|
}],
|
|
);
|
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report =
|
|
runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap");
|
|
match report {
|
|
CrashReport::VmTrap { trap } => {
|
|
assert_eq!(trap.code, TRAP_TYPE);
|
|
assert!(trap.message.contains("Expected integer at index 0"));
|
|
}
|
|
other => panic!("expected VmTrap crash report, got {:?}", other),
|
|
}
|
|
assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn tick_memcard_slot_roundtrip_for_game_profile() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
runtime.mount_fs(Box::new(MemFsBackend::default()));
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble(
|
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT",
|
|
)
|
|
.expect("assemble");
|
|
let program = serialized_single_function_module_with_consts(
|
|
code,
|
|
vec![ConstantPoolEntry::String("6869".into())], // "hi" in hex
|
|
vec![
|
|
SyscallDecl {
|
|
module: "mem".into(),
|
|
name: "slot_write".into(),
|
|
version: 1,
|
|
arg_slots: 3,
|
|
ret_slots: 2,
|
|
},
|
|
SyscallDecl {
|
|
module: "mem".into(),
|
|
name: "slot_commit".into(),
|
|
version: 1,
|
|
arg_slots: 1,
|
|
ret_slots: 1,
|
|
},
|
|
SyscallDecl {
|
|
module: "mem".into(),
|
|
name: "slot_read".into(),
|
|
version: 1,
|
|
arg_slots: 3,
|
|
ret_slots: 3,
|
|
},
|
|
],
|
|
);
|
|
let cartridge = Cartridge {
|
|
app_id: 42,
|
|
title: "Memcard Game".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode: AppMode::Game,
|
|
capabilities: caps::FS,
|
|
program,
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
};
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "memcard roundtrip must not crash");
|
|
assert!(vm.is_halted());
|
|
assert_eq!(
|
|
vm.operand_stack_top(3),
|
|
vec![Value::Int64(2), Value::String("6869".into()), Value::Int64(0)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tick_memcard_access_is_denied_for_non_game_profile() {
|
|
let mut runtime = VirtualMachineRuntime::new(None);
|
|
let mut vm = VirtualMachine::default();
|
|
let mut hardware = Hardware::new();
|
|
let signals = InputSignals::default();
|
|
let code = assemble("HOSTCALL 0\nHALT").expect("assemble");
|
|
let program = serialized_single_function_module(
|
|
code,
|
|
vec![SyscallDecl {
|
|
module: "mem".into(),
|
|
name: "slot_count".into(),
|
|
version: 1,
|
|
arg_slots: 0,
|
|
ret_slots: 2,
|
|
}],
|
|
);
|
|
let cartridge = Cartridge {
|
|
app_id: 101,
|
|
title: "System App".into(),
|
|
app_version: "1.0.0".into(),
|
|
app_mode: AppMode::System,
|
|
capabilities: caps::FS,
|
|
program,
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
};
|
|
|
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
|
assert!(report.is_none(), "non-game memcard call must return status");
|
|
assert!(vm.is_halted());
|
|
// top-first: count then status
|
|
assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]);
|
|
}
|