use std::time::Duration;
use frame_core::component::{
ChildArgument, ChildSpec, ComponentId, ComponentMeta, ServiceCapability, SupervisionPolicy,
};
use frame_core::event::LifecycleState;
use frame_core::registry::StopOutcome;
use frame_core::runtime::{ComponentRuntime, RuntimeError, RuntimePolicy, mailbox_integer};
const PRESENCE_BEAM: &[u8] = include_bytes!("fixtures/gcbif/graph_view_presence.beam");
const SUPPORT_BEAM: &[u8] = include_bytes!("fixtures/spike_worker_ffi.beam");
const CHILD: &str = "presence";
const LIVENESS_MESSAGE: i64 = 0;
const STOP_MESSAGE: i64 = 2;
fn policy() -> RuntimePolicy {
RuntimePolicy {
scheduler_threads: 2,
operation_timeout: Duration::from_secs(3),
}
}
fn component_id() -> ComponentId {
ComponentId::derive("frame.tests.runtime-wrapper", "presence")
}
fn meta() -> ComponentMeta {
ComponentMeta {
id: component_id(),
name: "wrapper acceptance presence component".to_owned(),
version: "0.1.0".to_owned(),
requires: Vec::new(),
provides: vec![ServiceCapability {
id: "frame.tests:runtime-wrapper@v1".to_owned(),
description: "real-arithmetic presence child driven through the wrapper".to_owned(),
}],
needs: Vec::new(),
children: vec![ChildSpec {
name: CHILD.to_owned(),
module: "graph_view_presence".to_owned(),
function: "run".to_owned(),
arguments: vec![ChildArgument::SupervisorPid],
liveness_message: LIVENESS_MESSAGE,
stop_message: STOP_MESSAGE,
}],
supervision: Some(SupervisionPolicy {
max_restarts: 3,
window: Duration::from_secs(30),
}),
}
}
#[test]
fn compose_refuses_zero_scheduler_threads() {
let refused = ComponentRuntime::compose(RuntimePolicy {
scheduler_threads: 0,
operation_timeout: Duration::from_secs(3),
});
assert!(matches!(refused, Err(RuntimeError::ZeroSchedulerThreads)));
}
#[test]
fn compose_refuses_zero_operation_timeout() {
let refused = ComponentRuntime::compose(RuntimePolicy {
scheduler_threads: 2,
operation_timeout: Duration::ZERO,
});
assert!(matches!(refused, Err(RuntimeError::ZeroOperationTimeout)));
}
#[test]
fn full_lifecycle_runs_through_the_wrapper_alone() -> Result<(), Box<dyn std::error::Error>> {
let runtime = ComponentRuntime::compose(policy())?;
let support = runtime.load_support_module(SUPPORT_BEAM)?;
let id = component_id();
runtime
.registry()
.register(meta(), PRESENCE_BEAM.to_vec())?;
runtime.registry().start(id)?;
let status = runtime
.registry()
.status(id)?
.ok_or("component status missing after start")?;
assert_eq!(status.state, LifecycleState::Running);
assert_eq!(runtime.registry().probe_child(id, CHILD)?, 0);
let entity_ack = mailbox_integer(b"wrapper-acceptance-entity-id", STOP_MESSAGE + 1)?;
assert!(entity_ack > STOP_MESSAGE);
runtime.registry().send_child(id, CHILD, entity_ack)?;
assert_eq!(runtime.registry().probe_child(id, CHILD)?, 1);
assert_eq!(runtime.registry().stop(id)?, StopOutcome::Stopped);
assert_eq!(runtime.live_process_count(), 0);
runtime.unload_support_module(support)?;
runtime.shutdown()?;
Ok(())
}
#[test]
fn support_module_load_refuses_invalid_bytecode() -> Result<(), Box<dyn std::error::Error>> {
let runtime = ComponentRuntime::compose(policy())?;
let refused = runtime.load_support_module(b"this is not BEAM bytecode");
assert!(matches!(
refused,
Err(RuntimeError::SupportModuleLoad { .. })
));
runtime.shutdown()?;
Ok(())
}
#[test]
fn support_module_unload_verifies_and_refuses_double_unload()
-> Result<(), Box<dyn std::error::Error>> {
let runtime = ComponentRuntime::compose(policy())?;
let first = runtime.load_support_module(SUPPORT_BEAM)?;
let second = runtime.load_support_module(SUPPORT_BEAM)?;
runtime.unload_support_module(first)?;
let refused = runtime.unload_support_module(second);
assert!(matches!(
refused,
Err(RuntimeError::SupportModuleDelete { .. })
));
runtime.shutdown()?;
Ok(())
}
#[test]
fn shutdown_refuses_process_residue() -> Result<(), Box<dyn std::error::Error>> {
let runtime = ComponentRuntime::compose(policy())?;
let id = component_id();
runtime
.registry()
.register(meta(), PRESENCE_BEAM.to_vec())?;
runtime.registry().start(id)?;
assert!(runtime.live_process_count() > 0);
match runtime.shutdown() {
Err(RuntimeError::ProcessResidue { count }) => {
assert!(count > 0, "residue refusal must report the live count");
Ok(())
}
Err(other) => Err(format!("expected ProcessResidue, got {other}").into()),
Ok(()) => Err("shutdown accepted a runtime with live processes".into()),
}
}