feat(sandbox): enforce launcher lifecycle ordering
This commit is contained in:
@@ -78,6 +78,22 @@ FD、栈和其他信号仍按原退出状态记录,缺少专门证据时不猜
|
||||
工件只能导出规格已申请的路径,单文件与已导出总量均受限;这些字段检查不能替代实际文件安全检查。
|
||||
准备失败和清理失败分别保留,真实进程树终止、资源回收与 VM 验收仍待后续阶段实现。
|
||||
|
||||
## 启动生命周期顺序
|
||||
|
||||
`launcher/lifecycle.rs` 提供独立的状态约束:已校验 → 准备 → cgroup/限额确认 → namespace
|
||||
确认 → 运行根确认 → 身份确认 → seccomp 确认 → exec 放行 → exec 确认 → 等待。
|
||||
放行与 exec 成功确认分别记录;主进程退出只进入终止阶段,直到整个任务树为空且启动器/init
|
||||
子进程已回收,才能进行最终计量、输出/工件收集和清理。清理成功后状态才结束。
|
||||
|
||||
取消、限额或操作失败停止推进初始化;乱序/重复阶段消息记录协议错误并进入终止流程。
|
||||
即使调用方忽略错误,也无法继续这次执行的 exec 放行。计量阶段的错误不恢复已回收的任务,
|
||||
也不跳过剩余收集;启动/监督故障与清理故障独立保留,重试清理成功不抹掉原始错误。
|
||||
最先停止的原因只用于调度诊断,最终执行分类仍由结果模型依据全部观察确定。
|
||||
|
||||
事件仅由可信监督器在核验同一任务的实际操作后产生,不可从执行请求直接反序列化。
|
||||
该模块不执行系统调用、不证明隔离、不产生执行许可;有界 IPC、单线程启动器、真实终止与
|
||||
清理仍待实现和独立 VM 验证。当前 CLI `run` 保持拒绝执行。
|
||||
|
||||
## 可信运行配置与启动前检查
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Monotone lifecycle ordering for the future trusted supervisor.
|
||||
//!
|
||||
//! Callers must produce events from verified operations on the same task. These
|
||||
//! in-memory types cannot authenticate OS observations and are deliberately not
|
||||
//! deserializable. In particular, reaching FilterReady is no execution permit.
|
||||
|
||||
use crate::outcome::{ErrorCode, ExecutionError, ExecutionStage, LimitKind};
|
||||
use crate::spec::ValidatedSpec;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
Validated,
|
||||
Preparing,
|
||||
CgroupReady,
|
||||
NamespacesReady,
|
||||
RootReady,
|
||||
IdentityReady,
|
||||
FilterReady,
|
||||
ExecReleased,
|
||||
Waiting,
|
||||
Terminating,
|
||||
Accounting,
|
||||
Cleaning,
|
||||
Finished,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Operation currently outstanding; cleanup failures have their own record.
|
||||
pub fn stage(self) -> Option<ExecutionStage> {
|
||||
match self {
|
||||
Self::Validated => Some(ExecutionStage::Preparation),
|
||||
Self::Preparing => Some(ExecutionStage::Cgroup),
|
||||
Self::CgroupReady => Some(ExecutionStage::Namespaces),
|
||||
Self::NamespacesReady => Some(ExecutionStage::RootFilesystem),
|
||||
Self::RootReady => Some(ExecutionStage::Identity),
|
||||
Self::IdentityReady => Some(ExecutionStage::Seccomp),
|
||||
Self::FilterReady | Self::ExecReleased => Some(ExecutionStage::Exec),
|
||||
Self::Waiting | Self::Terminating => Some(ExecutionStage::Wait),
|
||||
Self::Accounting => Some(ExecutionStage::Accounting),
|
||||
Self::Cleaning => Some(ExecutionStage::Cleanup),
|
||||
Self::Finished => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
BeginPreparation,
|
||||
/// Child remains blocked; task enrollment and limits are verified.
|
||||
CgroupConfirmed,
|
||||
NamespacesConfirmed,
|
||||
RootConfirmed,
|
||||
/// UID/GID, supplementary groups, capabilities and no_new_privs checked.
|
||||
IdentityConfirmed,
|
||||
FilterConfirmed,
|
||||
/// Trusted control transport has released the exec gate, not exec success.
|
||||
ExecGateReleased,
|
||||
/// The exec error channel confirmed success, distinct from gate release.
|
||||
ExecConfirmed,
|
||||
/// Does not imply that descendants have exited or output reached EOF.
|
||||
MainExited,
|
||||
Cancel,
|
||||
LimitObserved(LimitKind),
|
||||
Failed {
|
||||
code: ErrorCode,
|
||||
errno: Option<u16>,
|
||||
},
|
||||
/// Entire task tree empty and launcher/init children reaped.
|
||||
TreeReaped,
|
||||
/// Final accounting and bounded output/artifact collection settled,
|
||||
/// including explicit unavailable/error records when collection failed.
|
||||
CollectionFinished,
|
||||
CleanupSucceeded,
|
||||
CleanupFailed {
|
||||
errno: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
/// First reason to stop scheduling execution, not a judge/result classifier.
|
||||
/// Later cancellation and faults remain independently observable.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum StopReason {
|
||||
MainExited,
|
||||
Cancelled,
|
||||
Limit(LimitKind),
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TransitionError {
|
||||
pub state: State,
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TransitionError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
formatter,
|
||||
"invalid lifecycle event {:?} in {:?}",
|
||||
self.event, self.state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TransitionError {}
|
||||
|
||||
/// Private fields and no Clone/Deserialize keep each attempt's transitions monotone.
|
||||
/// Retains at most one primary fault and one independent cleanup fault.
|
||||
#[derive(Debug)]
|
||||
pub struct Lifecycle {
|
||||
run_id: String,
|
||||
state: State,
|
||||
stop_reason: Option<StopReason>,
|
||||
exec_released: bool,
|
||||
cancel_requested: bool,
|
||||
failure: Option<ExecutionError>,
|
||||
cleanup_failure: Option<ExecutionError>,
|
||||
}
|
||||
|
||||
impl Lifecycle {
|
||||
pub fn new(spec: &ValidatedSpec) -> Self {
|
||||
Self {
|
||||
run_id: spec.spec().run_id.clone(),
|
||||
state: State::Validated,
|
||||
stop_reason: None,
|
||||
exec_released: false,
|
||||
cancel_requested: false,
|
||||
failure: None,
|
||||
cleanup_failure: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_id(&self) -> &str {
|
||||
&self.run_id
|
||||
}
|
||||
pub fn state(&self) -> State {
|
||||
self.state
|
||||
}
|
||||
pub fn stop_reason(&self) -> Option<StopReason> {
|
||||
self.stop_reason
|
||||
}
|
||||
pub fn exec_released(&self) -> bool {
|
||||
self.exec_released
|
||||
}
|
||||
pub fn cancel_requested(&self) -> bool {
|
||||
self.cancel_requested
|
||||
}
|
||||
pub fn failure(&self) -> Option<&ExecutionError> {
|
||||
self.failure.as_ref()
|
||||
}
|
||||
pub fn cleanup_failure(&self) -> Option<&ExecutionError> {
|
||||
self.cleanup_failure.as_ref()
|
||||
}
|
||||
|
||||
/// An invalid live event latches a protocol fault and requests termination;
|
||||
/// ignoring the returned error cannot subsequently advance to exec.
|
||||
/// This only records ordering: the caller must still perform OS termination.
|
||||
pub fn apply(&mut self, event: Event) -> Result<(), TransitionError> {
|
||||
let previous = self.state;
|
||||
if previous == State::Finished {
|
||||
return Err(TransitionError {
|
||||
state: previous,
|
||||
event,
|
||||
});
|
||||
}
|
||||
match event {
|
||||
Event::Cancel => {
|
||||
self.cancel_requested = true;
|
||||
self.request_stop(StopReason::Cancelled);
|
||||
return Ok(());
|
||||
}
|
||||
Event::LimitObserved(kind) => {
|
||||
self.request_stop(StopReason::Limit(kind));
|
||||
return Ok(());
|
||||
}
|
||||
Event::Failed { code, errno } if valid_errno(errno) => {
|
||||
self.record_failure(code, errno);
|
||||
return Ok(());
|
||||
}
|
||||
Event::CleanupFailed { errno } if previous == State::Cleaning && valid_errno(errno) => {
|
||||
self.record_failure(ErrorCode::CleanupIncomplete, errno);
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let next = match (previous, event) {
|
||||
(State::Validated, Event::BeginPreparation) => Some(State::Preparing),
|
||||
(State::Preparing, Event::CgroupConfirmed) => Some(State::CgroupReady),
|
||||
(State::CgroupReady, Event::NamespacesConfirmed) => Some(State::NamespacesReady),
|
||||
(State::NamespacesReady, Event::RootConfirmed) => Some(State::RootReady),
|
||||
(State::RootReady, Event::IdentityConfirmed) => Some(State::IdentityReady),
|
||||
(State::IdentityReady, Event::FilterConfirmed) => Some(State::FilterReady),
|
||||
(State::FilterReady, Event::ExecGateReleased) => Some(State::ExecReleased),
|
||||
(State::ExecReleased, Event::ExecConfirmed) => Some(State::Waiting),
|
||||
(State::Waiting, Event::MainExited) => {
|
||||
self.request_stop(StopReason::MainExited);
|
||||
return Ok(());
|
||||
}
|
||||
(State::Terminating, Event::TreeReaped) => Some(State::Accounting),
|
||||
(State::Accounting, Event::CollectionFinished) => Some(State::Cleaning),
|
||||
(State::Cleaning, Event::CleanupSucceeded) => Some(State::Finished),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(next) = next {
|
||||
self.state = next;
|
||||
if next == State::ExecReleased {
|
||||
self.exec_released = true;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
self.record_failure(ErrorCode::ProtocolFailed, None);
|
||||
Err(TransitionError {
|
||||
state: previous,
|
||||
event,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn request_stop(&mut self, reason: StopReason) {
|
||||
self.stop_reason.get_or_insert(reason);
|
||||
match self.state {
|
||||
State::Terminating | State::Accounting | State::Cleaning | State::Finished => {}
|
||||
_ => self.state = State::Terminating,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_failure(&mut self, code: ErrorCode, errno: Option<u16>) {
|
||||
if let Some(stage) = self.state.stage() {
|
||||
let error = ExecutionError { stage, code, errno };
|
||||
if self.state == State::Cleaning {
|
||||
self.cleanup_failure.get_or_insert(error);
|
||||
} else {
|
||||
self.failure.get_or_insert(error);
|
||||
}
|
||||
// Accounting failures do not skip remaining collection. Once the
|
||||
// tree is reaped, later faults must never restore a live-task phase.
|
||||
self.request_stop(StopReason::Failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_errno(errno: Option<u16>) -> bool {
|
||||
errno.is_none_or(|value| (1..=4095).contains(&value))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Trusted supervisor/launcher coordination, separate from user stdio.
|
||||
//!
|
||||
//! Lifecycle events describe completed operations, not commands received from
|
||||
//! an untrusted request. OS operations and authenticated bounded IPC are pending;
|
||||
//! this module grants no execution authority and does not change CLI refusal.
|
||||
|
||||
pub mod lifecycle;
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Linux execution boundary: policy/contract checks and read-only observations.
|
||||
//!
|
||||
//! JS-01 defines execution contracts; JS-02..04 add the launcher, isolation and limits.
|
||||
//! Execution contracts and lifecycle ordering precede OS launcher/isolation work.
|
||||
//! Until that work is verified, the CLI cannot execute any user program.
|
||||
|
||||
pub mod launcher;
|
||||
pub mod outcome;
|
||||
pub mod probe;
|
||||
pub mod runtime;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
use kf_sandbox::launcher::lifecycle::{Event, Lifecycle, State, StopReason};
|
||||
use kf_sandbox::outcome::{ErrorCode, ExecutionStage, LimitKind};
|
||||
use kf_sandbox::spec::{ExecutionSpec, SpecPolicy};
|
||||
|
||||
const STARTUP: [Event; 8] = [
|
||||
Event::BeginPreparation,
|
||||
Event::CgroupConfirmed,
|
||||
Event::NamespacesConfirmed,
|
||||
Event::RootConfirmed,
|
||||
Event::IdentityConfirmed,
|
||||
Event::FilterConfirmed,
|
||||
Event::ExecGateReleased,
|
||||
Event::ExecConfirmed,
|
||||
];
|
||||
|
||||
fn lifecycle(prefix: usize) -> Lifecycle {
|
||||
let spec: ExecutionSpec =
|
||||
serde_json::from_str(include_str!("fixtures/spec.valid.json")).unwrap();
|
||||
let spec = spec.validate(&SpecPolicy::default()).unwrap();
|
||||
let mut task = Lifecycle::new(&spec);
|
||||
assert_eq!(task.run_id(), spec.spec().run_id);
|
||||
for event in &STARTUP[..prefix] {
|
||||
task.apply(*event).unwrap();
|
||||
}
|
||||
task
|
||||
}
|
||||
|
||||
fn settle(task: &mut Lifecycle) {
|
||||
task.apply(Event::TreeReaped).unwrap();
|
||||
task.apply(Event::CollectionFinished).unwrap();
|
||||
task.apply(Event::CleanupSucceeded).unwrap();
|
||||
assert_eq!(task.state(), State::Finished);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialization_exec_confirmation_and_tree_reaping_are_distinct() {
|
||||
let mut task = lifecycle(6);
|
||||
assert_eq!(task.state(), State::FilterReady);
|
||||
assert!(!task.exec_released());
|
||||
task.apply(Event::ExecGateReleased).unwrap();
|
||||
assert_eq!(task.state(), State::ExecReleased);
|
||||
assert!(task.exec_released());
|
||||
task.apply(Event::ExecConfirmed).unwrap();
|
||||
assert_eq!(task.state(), State::Waiting);
|
||||
task.apply(Event::MainExited).unwrap();
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.stop_reason(), Some(StopReason::MainExited));
|
||||
settle(&mut task);
|
||||
assert!(task.failure().is_none());
|
||||
assert!(task.cleanup_failure().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_incomplete_startup_prefix_rejects_and_latches_early_exec() {
|
||||
for prefix in 0..6 {
|
||||
let mut task = lifecycle(prefix);
|
||||
assert!(task.apply(Event::ExecGateReleased).is_err());
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.failure().unwrap().code, ErrorCode::ProtocolFailed);
|
||||
// Ignoring the error and submitting the omitted confirmations cannot
|
||||
// revive this attempt or open its exec gate.
|
||||
for event in &STARTUP[prefix..] {
|
||||
assert!(task.apply(*event).is_err());
|
||||
}
|
||||
assert!(!task.exec_released());
|
||||
settle(&mut task);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicated_or_out_of_order_security_confirmation_aborts() {
|
||||
for (prefix, event) in [
|
||||
(1, Event::FilterConfirmed),
|
||||
(2, Event::CgroupConfirmed),
|
||||
(3, Event::IdentityConfirmed),
|
||||
(4, Event::RootConfirmed),
|
||||
(6, Event::FilterConfirmed),
|
||||
] {
|
||||
let mut task = lifecycle(prefix);
|
||||
assert!(task.apply(event).is_err());
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.stop_reason(), Some(StopReason::Failure));
|
||||
assert!(!task.exec_released());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faults_preserve_the_outstanding_stage_and_cannot_restart_execution() {
|
||||
let stages = [
|
||||
ExecutionStage::Preparation,
|
||||
ExecutionStage::Cgroup,
|
||||
ExecutionStage::Namespaces,
|
||||
ExecutionStage::RootFilesystem,
|
||||
ExecutionStage::Identity,
|
||||
ExecutionStage::Seccomp,
|
||||
ExecutionStage::Exec,
|
||||
ExecutionStage::Exec,
|
||||
ExecutionStage::Wait,
|
||||
];
|
||||
for (prefix, stage) in stages.into_iter().enumerate() {
|
||||
let mut task = lifecycle(prefix);
|
||||
task.apply(Event::Failed {
|
||||
code: ErrorCode::OperationFailed,
|
||||
errno: Some(13),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.failure().unwrap().stage, stage);
|
||||
assert_eq!(task.failure().unwrap().errno, Some(13));
|
||||
assert!(task.apply(Event::ExecGateReleased).is_err());
|
||||
assert_eq!(task.failure().unwrap().code, ErrorCode::OperationFailed);
|
||||
settle(&mut task);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_at_every_pre_exec_boundary_is_idempotent_and_never_execs() {
|
||||
for prefix in 0..=6 {
|
||||
let mut task = lifecycle(prefix);
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert!(task.cancel_requested());
|
||||
assert_eq!(task.stop_reason(), Some(StopReason::Cancelled));
|
||||
assert!(!task.exec_released());
|
||||
settle(&mut task);
|
||||
assert!(task.failure().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_after_release_still_requires_tree_reaping() {
|
||||
for prefix in [7, 8] {
|
||||
let mut task = lifecycle(prefix);
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
assert!(task.exec_released());
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
settle(&mut task);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_exit_does_not_authorize_early_accounting_or_cleanup() {
|
||||
for event in [Event::CollectionFinished, Event::CleanupSucceeded] {
|
||||
let mut task = lifecycle(8);
|
||||
task.apply(Event::MainExited).unwrap();
|
||||
assert!(task.apply(event).is_err());
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.failure().unwrap().stage, ExecutionStage::Wait);
|
||||
settle(&mut task);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_fault_is_independent_and_retry_does_not_erase_diagnostics() {
|
||||
let mut task = lifecycle(3);
|
||||
task.apply(Event::Failed {
|
||||
code: ErrorCode::OperationFailed,
|
||||
errno: Some(1),
|
||||
})
|
||||
.unwrap();
|
||||
task.apply(Event::TreeReaped).unwrap();
|
||||
task.apply(Event::CollectionFinished).unwrap();
|
||||
task.apply(Event::CleanupFailed { errno: Some(16) })
|
||||
.unwrap();
|
||||
assert_eq!(task.state(), State::Cleaning);
|
||||
assert_eq!(
|
||||
task.failure().unwrap().stage,
|
||||
ExecutionStage::RootFilesystem
|
||||
);
|
||||
assert_eq!(task.failure().unwrap().errno, Some(1));
|
||||
assert_eq!(
|
||||
task.cleanup_failure().unwrap().stage,
|
||||
ExecutionStage::Cleanup
|
||||
);
|
||||
assert_eq!(
|
||||
task.cleanup_failure().unwrap().code,
|
||||
ErrorCode::CleanupIncomplete
|
||||
);
|
||||
assert_eq!(task.cleanup_failure().unwrap().errno, Some(16));
|
||||
task.apply(Event::CleanupSucceeded).unwrap();
|
||||
assert_eq!(task.state(), State::Finished);
|
||||
assert!(task.cleanup_failure().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accounting_faults_cannot_restore_live_task_phases_or_skip_collection() {
|
||||
let mut task = lifecycle(8);
|
||||
task.apply(Event::MainExited).unwrap();
|
||||
task.apply(Event::TreeReaped).unwrap();
|
||||
task.apply(Event::Failed {
|
||||
code: ErrorCode::MeasurementUnavailable,
|
||||
errno: None,
|
||||
})
|
||||
.unwrap();
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
assert_eq!(task.state(), State::Accounting);
|
||||
assert_eq!(task.failure().unwrap().stage, ExecutionStage::Accounting);
|
||||
assert!(task.cancel_requested());
|
||||
assert_eq!(task.stop_reason(), Some(StopReason::MainExited));
|
||||
assert!(task.apply(Event::CleanupSucceeded).is_err());
|
||||
assert_eq!(task.state(), State::Accounting);
|
||||
task.apply(Event::CollectionFinished).unwrap();
|
||||
task.apply(Event::CleanupSucceeded).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_errno_becomes_a_protocol_fault_without_storing_invalid_values() {
|
||||
for errno in [0, 4096, u16::MAX] {
|
||||
let mut task = lifecycle(1);
|
||||
assert!(
|
||||
task.apply(Event::Failed {
|
||||
code: ErrorCode::OperationFailed,
|
||||
errno: Some(errno),
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(task.state(), State::Terminating);
|
||||
assert_eq!(task.failure().unwrap().code, ErrorCode::ProtocolFailed);
|
||||
assert_eq!(task.failure().unwrap().errno, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_stop_and_late_cancel_preserve_the_first_stop_reason() {
|
||||
let mut task = lifecycle(8);
|
||||
task.apply(Event::LimitObserved(LimitKind::CpuTime))
|
||||
.unwrap();
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
task.apply(Event::LimitObserved(LimitKind::Memory)).unwrap();
|
||||
assert_eq!(
|
||||
task.stop_reason(),
|
||||
Some(StopReason::Limit(LimitKind::CpuTime))
|
||||
);
|
||||
assert!(task.cancel_requested());
|
||||
settle(&mut task);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_tasks_reject_all_events_without_reopening_or_mutating_results() {
|
||||
let mut task = lifecycle(0);
|
||||
task.apply(Event::Cancel).unwrap();
|
||||
settle(&mut task);
|
||||
for event in STARTUP.into_iter().chain([
|
||||
Event::Cancel,
|
||||
Event::LimitObserved(LimitKind::Memory),
|
||||
Event::CleanupFailed { errno: Some(1) },
|
||||
Event::CleanupSucceeded,
|
||||
]) {
|
||||
assert!(task.apply(event).is_err());
|
||||
assert_eq!(task.state(), State::Finished);
|
||||
assert_eq!(task.stop_reason(), Some(StopReason::Cancelled));
|
||||
assert!(!task.exec_released());
|
||||
assert!(task.failure().is_none());
|
||||
assert!(task.cleanup_failure().is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user