645 lines
23 KiB
Rust
645 lines
23 KiB
Rust
use crate::VirtualMachineRuntime;
|
|
use crate::fs::{FsState, VirtualFS};
|
|
use crate::os::{
|
|
FsFacade, GameLifecycleEvent, LifecycleFacade, SessionsFacade, VmFacade, WindowFacade,
|
|
};
|
|
use crate::process::ProcessManager;
|
|
use crate::services::foreground::ForegroundStack;
|
|
use crate::services::memcard::MemcardService;
|
|
use crate::services::vm_session::VmSessionRegistry;
|
|
use crate::services::windows::WindowManager;
|
|
use crate::task::TaskManager;
|
|
use prometeu_hal::log::{LogEvent, LogLevel, LogService, LogSource};
|
|
use prometeu_hal::telemetry::CertificationConfig;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
pub struct SystemOS {
|
|
pub(super) vm_runtime: VirtualMachineRuntime,
|
|
pub(super) process_manager: ProcessManager,
|
|
pub(super) task_manager: TaskManager,
|
|
pub(super) foreground_stack: ForegroundStack,
|
|
pub(super) vm_sessions: VmSessionRegistry,
|
|
pub(super) game_lifecycle_events: Vec<GameLifecycleEvent>,
|
|
pub(super) window_manager: WindowManager,
|
|
pub(super) log_service: LogService,
|
|
pub(super) fs: VirtualFS,
|
|
pub(super) fs_state: FsState,
|
|
pub(super) memcard: MemcardService,
|
|
pub(super) open_files: HashMap<u32, String>,
|
|
pub(super) next_handle: u32,
|
|
}
|
|
|
|
impl SystemOS {
|
|
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
|
let log_service = LogService::new(4096);
|
|
let mut os = Self {
|
|
vm_runtime: VirtualMachineRuntime::new_with_log_counter(
|
|
cap_config,
|
|
Arc::clone(&log_service.logs_count),
|
|
),
|
|
process_manager: ProcessManager::new(),
|
|
task_manager: TaskManager::new(),
|
|
foreground_stack: ForegroundStack::new(),
|
|
vm_sessions: VmSessionRegistry::new(),
|
|
game_lifecycle_events: Vec::new(),
|
|
window_manager: WindowManager::new(),
|
|
log_service,
|
|
fs: VirtualFS::new(),
|
|
fs_state: FsState::Unmounted,
|
|
memcard: MemcardService::new(),
|
|
open_files: HashMap::new(),
|
|
next_handle: 1,
|
|
};
|
|
|
|
os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string());
|
|
|
|
os
|
|
}
|
|
|
|
pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) {
|
|
self.vm_runtime.log(&mut self.log_service, level, source, tag, msg);
|
|
}
|
|
|
|
pub fn trace(&mut self, source: LogSource, msg: String) {
|
|
self.log(LogLevel::Trace, source, 0, msg);
|
|
}
|
|
|
|
pub fn trace_tag(&mut self, source: LogSource, tag: u16, msg: String) {
|
|
self.log(LogLevel::Trace, source, tag, msg);
|
|
}
|
|
|
|
pub fn debug(&mut self, source: LogSource, msg: String) {
|
|
self.log(LogLevel::Debug, source, 0, msg);
|
|
}
|
|
|
|
pub fn debug_tag(&mut self, source: LogSource, tag: u16, msg: String) {
|
|
self.log(LogLevel::Debug, source, tag, msg);
|
|
}
|
|
|
|
pub fn info(&mut self, source: LogSource, msg: String) {
|
|
self.log(LogLevel::Info, source, 0, msg);
|
|
}
|
|
|
|
pub fn info_tag(&mut self, source: LogSource, tag: u16, msg: String) {
|
|
self.log(LogLevel::Info, source, tag, msg);
|
|
}
|
|
|
|
pub fn warn(&mut self, source: LogSource, msg: String) {
|
|
self.log(LogLevel::Warn, source, 0, msg);
|
|
}
|
|
|
|
pub fn warn_tag(&mut self, source: LogSource, tag: u16, msg: String) {
|
|
self.log(LogLevel::Warn, source, tag, msg);
|
|
}
|
|
|
|
pub fn error(&mut self, source: LogSource, msg: String) {
|
|
self.log(LogLevel::Error, source, 0, msg);
|
|
}
|
|
|
|
pub fn error_tag(&mut self, source: LogSource, tag: u16, msg: String) {
|
|
self.log(LogLevel::Error, source, tag, msg);
|
|
}
|
|
|
|
pub fn recent_logs(&self, n: usize) -> Vec<LogEvent> {
|
|
self.log_service.get_recent(n)
|
|
}
|
|
|
|
pub fn logs_after(&self, seq: u64) -> Vec<LogEvent> {
|
|
self.log_service.get_after(seq)
|
|
}
|
|
|
|
pub fn frame_index(&mut self) -> u64 {
|
|
self.vm().logical_frame_index()
|
|
}
|
|
|
|
pub fn lifecycle(&mut self) -> LifecycleFacade<'_> {
|
|
LifecycleFacade { os: self }
|
|
}
|
|
|
|
pub fn sessions(&mut self) -> SessionsFacade<'_> {
|
|
SessionsFacade { os: self }
|
|
}
|
|
|
|
pub fn vm(&mut self) -> VmFacade<'_> {
|
|
VmFacade { os: self }
|
|
}
|
|
|
|
pub fn fs(&mut self) -> FsFacade<'_> {
|
|
FsFacade { os: self }
|
|
}
|
|
|
|
pub fn windows(&mut self) -> WindowFacade<'_> {
|
|
WindowFacade { os: self }
|
|
}
|
|
|
|
pub(super) fn clear_cartridge_service_state(&mut self) {
|
|
self.open_files.clear();
|
|
self.next_handle = 1;
|
|
self.memcard.clear_all_staging();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::os::{GameLifecycleEventKind, LifecycleError, LifecycleOperation, SessionError};
|
|
use crate::process::{ProcessId, ProcessKind, ProcessState};
|
|
use crate::services::foreground::{ForegroundOwner, ResidentGame, ResidentGameState};
|
|
use crate::task::{TaskId, TaskState};
|
|
use prometeu_hal::app_mode::AppMode;
|
|
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
|
use prometeu_hal::syscalls::caps;
|
|
|
|
fn process_state_for_task(os: &SystemOS, task_id: TaskId) -> ProcessState {
|
|
let task = os.task_manager.get(task_id).expect("task should exist");
|
|
os.process_manager.get(task.process_id).expect("process should exist").state
|
|
}
|
|
|
|
fn session_test_cartridge(app_mode: AppMode, app_id: u32, title: &str) -> Cartridge {
|
|
Cartridge {
|
|
app_id,
|
|
title: title.to_string(),
|
|
app_version: "1.0.0".to_string(),
|
|
app_mode,
|
|
capabilities: caps::NONE,
|
|
program: vec![],
|
|
assets: AssetsPayloadSource::empty(),
|
|
asset_table: vec![],
|
|
preload: vec![],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_foreground_task_marks_task_and_process_running() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
|
os.lifecycle().set_foreground_task(task_id).expect("foreground should succeed");
|
|
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Foreground
|
|
);
|
|
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
|
|
}
|
|
|
|
#[test]
|
|
fn foreground_stack_starts_at_hub() {
|
|
let os = SystemOS::new(None);
|
|
|
|
assert_eq!(os.foreground_stack.owner(), ForegroundOwner::Hub);
|
|
assert_eq!(os.foreground_stack.resident_game(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn game_session_registers_single_resident_game() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id));
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame { task_id, state: ResidentGameState::Foreground })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn second_resident_game_is_rejected() {
|
|
let mut os = SystemOS::new(None);
|
|
let first = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
let process_id = os.process_manager.spawn_vm_game(43, "Second");
|
|
let second = os.task_manager.create_game_task(process_id, 43, "Second");
|
|
|
|
assert_eq!(
|
|
os.lifecycle().set_game_foreground_task(second),
|
|
Err(LifecycleError::ResidentGameAlreadyExists { existing: first, requested: second })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_game_task_creates_vm_session() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
let sessions = os.sessions();
|
|
let session = sessions.vm_session_for_task(task_id).expect("VM session should exist");
|
|
|
|
assert_eq!(session.task_id, task_id);
|
|
assert_eq!(session.app_id, 42);
|
|
assert_eq!(session.title, "Sector Crawl");
|
|
assert_eq!(session.app_mode, AppMode::Game);
|
|
assert_eq!(sessions.vm_session_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_shell_task_creates_vm_session() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
let sessions = os.sessions();
|
|
let session = sessions.vm_session_for_task(task_id).expect("VM session should exist");
|
|
|
|
assert_eq!(session.task_id, task_id);
|
|
assert_eq!(session.app_id, 7);
|
|
assert_eq!(session.title, "Settings");
|
|
assert_eq!(session.app_mode, AppMode::Shell);
|
|
assert_eq!(sessions.vm_session_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn native_shell_task_does_not_create_vm_session() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_native_shell_task(7, "Settings");
|
|
|
|
let sessions = os.sessions();
|
|
|
|
assert!(sessions.vm_session_for_task(task_id).is_none());
|
|
assert_eq!(sessions.vm_session_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn same_resident_game_app_reuses_existing_vm_session() {
|
|
let mut os = SystemOS::new(None);
|
|
let first = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
let second = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
let sessions = os.sessions();
|
|
|
|
assert_eq!(second, first);
|
|
assert_eq!(
|
|
sessions.resident_game_session_for_app(42).map(|session| session.task_id),
|
|
Some(first)
|
|
);
|
|
assert_eq!(sessions.vm_session_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_sessions_hold_independent_runtime_state() {
|
|
let mut os = SystemOS::new(None);
|
|
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
|
|
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
|
|
let shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
{
|
|
let mut sessions = os.sessions();
|
|
let game_session =
|
|
sessions.vm_session_for_task_mut(game).expect("game VM session should exist");
|
|
game_session.runtime.tick_index = 100;
|
|
game_session.open_files.insert(1, "/game.dat".to_string());
|
|
}
|
|
|
|
{
|
|
let mut sessions = os.sessions();
|
|
let shell_session =
|
|
sessions.vm_session_for_task_mut(shell).expect("shell VM session should exist");
|
|
shell_session.runtime.tick_index = 5;
|
|
shell_session.open_files.insert(1, "/shell.dat".to_string());
|
|
}
|
|
|
|
let sessions = os.sessions();
|
|
let game_session =
|
|
sessions.vm_session_for_task(game).expect("game VM session should exist");
|
|
let shell_session =
|
|
sessions.vm_session_for_task(shell).expect("shell VM session should exist");
|
|
|
|
assert_eq!(game_session.runtime.tick_index, 100);
|
|
assert_eq!(shell_session.runtime.tick_index, 5);
|
|
assert_eq!(game_session.open_files.get(&1).map(String::as_str), Some("/game.dat"));
|
|
assert_eq!(shell_session.open_files.get(&1).map(String::as_str), Some("/shell.dat"));
|
|
}
|
|
|
|
#[test]
|
|
fn vm_debug_operations_target_foreground_shell_before_resident_game() {
|
|
let mut os = SystemOS::new(None);
|
|
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
|
|
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
|
|
let shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
{
|
|
let mut sessions = os.sessions();
|
|
let game_session =
|
|
sessions.vm_session_for_task_mut(game).expect("game VM session should exist");
|
|
game_session.vm.insert_breakpoint(10);
|
|
}
|
|
|
|
assert!(os.vm().insert_active_breakpoint(20));
|
|
assert_eq!(os.vm().active_breakpoints_list(), vec![20]);
|
|
|
|
let sessions = os.sessions();
|
|
assert_eq!(
|
|
sessions
|
|
.vm_session_for_task(game)
|
|
.expect("game VM session should exist")
|
|
.vm
|
|
.breakpoints_list(),
|
|
vec![10]
|
|
);
|
|
assert_eq!(
|
|
sessions
|
|
.vm_session_for_task(shell)
|
|
.expect("shell VM session should exist")
|
|
.vm
|
|
.breakpoints_list(),
|
|
vec![20]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn home_request_notifies_game_before_budget_suspends_it() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
os.lifecycle().request_home_from_game(task_id).expect("home request should succeed");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Game(task_id));
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame {
|
|
task_id,
|
|
state: ResidentGameState::PauseRequested { remaining_ticks: 1 },
|
|
})
|
|
);
|
|
assert_eq!(
|
|
os.lifecycle().pending_game_lifecycle_events()[0].kind,
|
|
GameLifecycleEventKind::Pause
|
|
);
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Foreground
|
|
);
|
|
|
|
assert!(os.lifecycle().advance_game_pause_budget(task_id).expect("budget should advance"));
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame { task_id, state: ResidentGameState::PausedSuspended })
|
|
);
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Suspended
|
|
);
|
|
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_foreground_keeps_game_resident_and_marks_it_suspended() {
|
|
let mut os = SystemOS::new(None);
|
|
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
os.lifecycle().request_home_from_game(game).expect("home request should succeed");
|
|
os.lifecycle().advance_game_pause_budget(game).expect("budget should expire");
|
|
let shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_shell_foreground_suspends_foreground_resident_game() {
|
|
let mut os = SystemOS::new(None);
|
|
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
let shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
|
|
);
|
|
assert_eq!(
|
|
os.task_manager.get(game).expect("game should exist").state,
|
|
TaskState::Suspended
|
|
);
|
|
assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended);
|
|
}
|
|
|
|
#[test]
|
|
fn native_shell_foreground_suspends_foreground_resident_game() {
|
|
let mut os = SystemOS::new(None);
|
|
let game = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
let shell = os.sessions().create_native_shell_task(7, "Settings");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Shell(shell));
|
|
assert_eq!(
|
|
os.lifecycle().resident_game(),
|
|
Some(ResidentGame { task_id: game, state: ResidentGameState::PausedSuspended })
|
|
);
|
|
assert_eq!(
|
|
os.task_manager.get(game).expect("game should exist").state,
|
|
TaskState::Suspended
|
|
);
|
|
assert_eq!(process_state_for_task(&os, game), ProcessState::Suspended);
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_exposes_process_kind_for_shell_tasks() {
|
|
let mut os = SystemOS::new(None);
|
|
let vm_shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
os.lifecycle().close_task(vm_shell).expect("vm shell should close");
|
|
|
|
let native_shell = os.sessions().create_native_shell_task(8, "Files");
|
|
|
|
assert_eq!(os.lifecycle().process_kind_for_task(vm_shell), Ok(ProcessKind::VmShell));
|
|
assert_eq!(
|
|
os.lifecycle().process_kind_for_task(native_shell),
|
|
Ok(ProcessKind::NativeShell)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn second_foreground_shell_is_rejected() {
|
|
let mut os = SystemOS::new(None);
|
|
let first = os.sessions().create_vm_shell_task(7, "Settings");
|
|
let process_id = os.process_manager.spawn_vm_shell(8, "Files");
|
|
let second = os.task_manager.create_shell_task(process_id, 8, "Files");
|
|
|
|
assert_eq!(
|
|
os.lifecycle().set_shell_foreground_task(second),
|
|
Err(LifecycleError::ForegroundShellAlreadyExists {
|
|
existing: first,
|
|
requested: second,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_shell_load_returns_typed_error_when_foreground_shell_exists() {
|
|
let mut os = SystemOS::new(None);
|
|
let first = os.sessions().create_vm_shell_task(7, "Settings");
|
|
let cartridge = session_test_cartridge(AppMode::Shell, 8, "Files");
|
|
|
|
let error = os.sessions().load_vm_cartridge(&cartridge).expect_err("load should fail");
|
|
|
|
match error {
|
|
SessionError::Lifecycle(LifecycleError::ForegroundShellAlreadyExists {
|
|
existing,
|
|
requested,
|
|
}) => {
|
|
assert_eq!(existing, first);
|
|
assert_ne!(requested, first);
|
|
}
|
|
other => panic!("expected foreground shell lifecycle error, got {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn vm_session_creation_returns_typed_error_for_missing_task() {
|
|
let mut os = SystemOS::new(None);
|
|
|
|
assert_eq!(
|
|
os.sessions().try_create_vm_session_for_task(TaskId(999)),
|
|
Err(SessionError::TaskNotFound(TaskId(999)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vm_session_creation_returns_typed_error_for_missing_process() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.task_manager.create_game_task(ProcessId(999), 42, "Broken");
|
|
|
|
assert_eq!(
|
|
os.sessions().try_create_vm_session_for_task(task_id),
|
|
Err(SessionError::ProcessNotFound(ProcessId(999)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn session_cartridge_initialization_returns_typed_error_for_missing_session() {
|
|
let mut os = SystemOS::new(None);
|
|
let process_id = os.process_manager.spawn_vm_game(42, "Sector Crawl");
|
|
let task_id = os.task_manager.create_game_task(process_id, 42, "Sector Crawl");
|
|
let cartridge = session_test_cartridge(AppMode::Game, 42, "Sector Crawl");
|
|
|
|
assert_eq!(
|
|
os.sessions().try_initialize_session_cartridge(task_id, &cartridge),
|
|
Err(SessionError::MissingVmSession(task_id))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn closing_foreground_shell_returns_to_hub() {
|
|
let mut os = SystemOS::new(None);
|
|
let shell = os.sessions().create_vm_shell_task(7, "Settings");
|
|
|
|
os.lifecycle().close_task(shell).expect("close should succeed");
|
|
|
|
assert_eq!(os.lifecycle().foreground_owner(), ForegroundOwner::Hub);
|
|
assert_eq!(
|
|
os.task_manager.get(shell).expect("shell should remain").state,
|
|
TaskState::Closed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn suspend_task_marks_task_and_process_suspended() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
|
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Suspended
|
|
);
|
|
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Suspended);
|
|
}
|
|
|
|
#[test]
|
|
fn resume_task_marks_suspended_task_foreground_and_process_running() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
os.lifecycle().suspend_task(task_id).expect("suspend should succeed");
|
|
os.lifecycle().resume_task(task_id).expect("resume should succeed");
|
|
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Foreground
|
|
);
|
|
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Running);
|
|
assert_eq!(
|
|
os.lifecycle().pending_game_lifecycle_events().last().map(|event| event.kind),
|
|
Some(GameLifecycleEventKind::ResumeForeground)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn close_task_marks_task_closed_and_process_stopped_without_removing_entities() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
let process_id = os.task_manager.get(task_id).expect("task should exist").process_id;
|
|
|
|
os.lifecycle().close_task(task_id).expect("close should succeed");
|
|
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should remain").state,
|
|
TaskState::Closed
|
|
);
|
|
assert_eq!(
|
|
os.process_manager.get(process_id).expect("process should remain").state,
|
|
ProcessState::Stopped
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn crash_task_marks_task_and_process_crashed() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
os.lifecycle().crash_task(task_id, None).expect("crash should succeed");
|
|
|
|
assert_eq!(
|
|
os.task_manager.get(task_id).expect("task should exist").state,
|
|
TaskState::Crashed
|
|
);
|
|
assert_eq!(process_state_for_task(&os, task_id), ProcessState::Crashed);
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_operation_missing_task_returns_typed_error() {
|
|
let mut os = SystemOS::new(None);
|
|
|
|
assert_eq!(
|
|
os.lifecycle().suspend_task(TaskId(999)),
|
|
Err(LifecycleError::TaskNotFound(TaskId(999)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_operation_missing_process_returns_typed_error() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
let process_id = os.task_manager.get(task_id).expect("task should exist").process_id;
|
|
|
|
os.process_manager.mark_stopped(process_id);
|
|
os.process_manager.remove_stopped();
|
|
|
|
assert_eq!(
|
|
os.lifecycle().suspend_task(task_id),
|
|
Err(LifecycleError::ProcessNotFound(process_id))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resume_task_from_non_suspended_state_returns_invalid_transition() {
|
|
let mut os = SystemOS::new(None);
|
|
let task_id = os.sessions().create_vm_game_task(42, "Sector Crawl");
|
|
|
|
assert_eq!(
|
|
os.lifecycle().resume_task(task_id),
|
|
Err(LifecycleError::InvalidTransition {
|
|
task_id,
|
|
from: TaskState::Foreground,
|
|
operation: LifecycleOperation::Resume,
|
|
})
|
|
);
|
|
}
|
|
}
|