// file: crates/ksp-job-api/unit_tests/lifecycle.rs // version: 2 fn new_lifecycle() -> std::option::Option { let id = match crate::JobId::new("job-001") { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::option::Option::None, }; let kind = match crate::JobKindCode::new("backfill_raw") { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::option::Option::None, }; return std::option::Option::Some(crate::JobLifecycle::new(id, kind)); } #[test] fn pre_002_lifecycle_accepts_every_planned_terminal_path() { for completion in [crate::JobCompletion::Complete, crate::JobCompletion::Partial] { let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => continue, }; assert!(lifecycle.start().is_ok()); assert!(lifecycle.complete(completion).is_ok()); assert_eq!(lifecycle.state(), crate::JobState::Completed(completion)); assert!(lifecycle.state().is_terminal()); assert_eq!(lifecycle.state().completion(), std::option::Option::Some(completion)); } let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => return, }; assert!(lifecycle.start().is_ok()); assert!(lifecycle.fail().is_ok()); assert_eq!(lifecycle.state(), crate::JobState::Failed); let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => return, }; assert!(lifecycle.mark_cancelled().is_ok()); assert_eq!(lifecycle.state(), crate::JobState::Cancelled); return; } #[test] fn pre_002_cancelling_allows_cancel_complete_or_fail() { for terminal in [crate::JobState::Cancelled, crate::JobState::Completed(crate::JobCompletion::Complete), crate::JobState::Failed] { let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => continue, }; assert!(lifecycle.start().is_ok()); assert!(lifecycle.mark_cancelling().is_ok()); let result = match terminal { crate::JobState::Cancelled => lifecycle.mark_cancelled(), crate::JobState::Completed(completion) => lifecycle.complete(completion), crate::JobState::Failed => lifecycle.fail(), _ => return, }; assert!(result.is_ok()); assert_eq!(lifecycle.state(), terminal); } return; } #[test] fn pre_002_invalid_transition_preserves_source_state_and_reports_safe_context() { let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => return, }; let rejected = lifecycle.complete(crate::JobCompletion::Complete); assert!(rejected.is_err()); assert_eq!(lifecycle.state(), crate::JobState::Created); let error = match rejected { std::result::Result::Err(value) => value, std::result::Result::Ok(_) => return, }; assert_eq!(error.code(), crate::ERROR_CODE_JOB_TRANSITION_INVALID); assert_eq!(error.context().len(), 2); assert_eq!(error.context()[0].key(), "source_state"); assert_eq!(error.context()[0].value(), "created"); assert_eq!(error.context()[1].key(), "target_state"); assert_eq!(error.context()[1].value(), "completed"); return; } #[test] fn pre_002_every_terminal_state_rejects_later_mutation() { let terminals = [crate::JobState::Completed(crate::JobCompletion::Complete), crate::JobState::Cancelled, crate::JobState::Failed]; for terminal in terminals { let lifecycle = new_lifecycle(); assert!(lifecycle.is_some()); let mut lifecycle = match lifecycle { std::option::Option::Some(value) => value, std::option::Option::None => continue, }; if terminal == crate::JobState::Cancelled { assert!(lifecycle.mark_cancelled().is_ok()); } else { assert!(lifecycle.start().is_ok()); let terminal_result = match terminal { crate::JobState::Completed(completion) => lifecycle.complete(completion), crate::JobState::Failed => lifecycle.fail(), _ => return, }; assert!(terminal_result.is_ok()); } let rejected = lifecycle.start(); assert!(rejected.is_err()); assert_eq!(lifecycle.state(), terminal); } return; } #[test] fn pre_002_transition_matrix_is_exact() { let states = [ crate::JobState::Created, crate::JobState::Running, crate::JobState::Cancelling, crate::JobState::Completed(crate::JobCompletion::Complete), crate::JobState::Cancelled, crate::JobState::Failed, ]; for source in states { for target in states { let expected = matches!( (source, target), (crate::JobState::Created, crate::JobState::Running) | (crate::JobState::Created, crate::JobState::Cancelled) | (crate::JobState::Running, crate::JobState::Cancelling) | (crate::JobState::Running, crate::JobState::Completed(_)) | (crate::JobState::Running, crate::JobState::Failed) | (crate::JobState::Cancelling, crate::JobState::Completed(_)) | (crate::JobState::Cancelling, crate::JobState::Cancelled) | (crate::JobState::Cancelling, crate::JobState::Failed) ); assert_eq!(super::allowed_transition(source, target), expected, "unexpected transition matrix result for {source:?} -> {target:?}"); } } return; }