v0.3.6-pre.003
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
// file: crates/ksp-job-api/src/error.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Error code used when a Job identifier violates its bounded safe-code contract.
|
||||
pub const ERROR_CODE_JOB_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_id_invalid");
|
||||
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
||||
pub const ERROR_CODE_JOB_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_kind_invalid");
|
||||
/// Error code used when a Job notification sequence cannot advance without wrapping.
|
||||
pub const ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("job_api", "job_notification_sequence_exhausted");
|
||||
/// Error code used when a requested Job lifecycle transition is not allowed.
|
||||
pub const ERROR_CODE_JOB_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("job_api", "job_transition_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-api/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -8,14 +8,15 @@
|
||||
//! Passive runtime-neutral lifecycle contracts for bounded KSP Jobs.
|
||||
//!
|
||||
//! This foundation owns validated Job identity, explicit lifecycle transitions
|
||||
//! and cooperative cancellation intent. Notification delivery, concrete Job
|
||||
//! behavior, runtime spawning, Transport, Store and Worker contracts remain
|
||||
//! outside this crate.
|
||||
//! cooperative cancellation intent and runtime-neutral latest-value observation.
|
||||
//! Concrete Job behavior, runtime spawning, Transport, Store and Worker contracts
|
||||
//! remain outside this crate.
|
||||
|
||||
mod cancellation;
|
||||
mod error;
|
||||
mod identity;
|
||||
mod lifecycle;
|
||||
mod notification;
|
||||
|
||||
/// Runtime-neutral cloneable token carrying cooperative cancellation intent.
|
||||
pub use self::cancellation::JobCancellationToken;
|
||||
@@ -23,6 +24,8 @@ pub use self::cancellation::JobCancellationToken;
|
||||
pub use self::error::ERROR_CODE_JOB_ID_INVALID;
|
||||
/// Error code used when a Job kind code violates its bounded safe-code contract.
|
||||
pub use self::error::ERROR_CODE_JOB_KIND_INVALID;
|
||||
/// Error code used when a Job notification sequence cannot advance without wrapping.
|
||||
pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;
|
||||
/// Error code used when a requested Job lifecycle transition is not allowed.
|
||||
pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;
|
||||
/// Bounded caller-supplied identity of one logical Job and its controlled resumptions.
|
||||
@@ -39,6 +42,14 @@ pub use self::lifecycle::JobCompletion;
|
||||
pub use self::lifecycle::JobLifecycle;
|
||||
/// Current lifecycle state of one bounded Job.
|
||||
pub use self::lifecycle::JobState;
|
||||
/// Latest complete observable value for one Job at a monotone sequence position.
|
||||
pub use self::notification::JobNotification;
|
||||
/// Monotone sequence attached to one latest-value Job notification stream.
|
||||
pub use self::notification::JobNotificationSequence;
|
||||
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
|
||||
pub use self::notification::JobSnapshotFuture;
|
||||
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
|
||||
pub use self::notification::JobSnapshotSource;
|
||||
/// Common KSP error type used by Job-facing contracts.
|
||||
pub use ksp_core_lib::Error;
|
||||
/// Stable structured code identifying a KSP error category and condition.
|
||||
|
||||
126
crates/ksp-job-api/src/notification.rs
Normal file
126
crates/ksp-job-api/src/notification.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
// file: crates/ksp-job-api/src/notification.rs
|
||||
// version: 1
|
||||
|
||||
/// Monotone sequence attached to one latest-value Job notification stream.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct JobNotificationSequence(u64);
|
||||
|
||||
impl JobNotificationSequence {
|
||||
/// Creates the initial sequence position for one Job snapshot stream.
|
||||
#[must_use]
|
||||
pub const fn initial() -> Self {
|
||||
return Self(0);
|
||||
}
|
||||
|
||||
/// Returns the opaque numeric position carried by this sequence.
|
||||
#[must_use]
|
||||
pub const fn value(&self) -> u64 {
|
||||
return self.0;
|
||||
}
|
||||
|
||||
/// Advances the sequence exactly once or reports exhaustion without wrapping.
|
||||
pub fn next(&self) -> crate::Result<Self> {
|
||||
let next = match self.0.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
crate::Error::new(crate::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED, "Job notification sequence exhausted")
|
||||
.with_context("sequence", self.0.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(Self(next));
|
||||
}
|
||||
|
||||
/// Reports whether this sequence is strictly newer than an observed sequence.
|
||||
#[must_use]
|
||||
pub const fn is_after(&self, observed: Self) -> bool {
|
||||
return self.0 > observed.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Latest complete observable value for one Job at a monotone sequence position.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct JobNotification<S> {
|
||||
id: crate::JobId,
|
||||
kind: crate::JobKindCode,
|
||||
sequence: crate::JobNotificationSequence,
|
||||
snapshot: S,
|
||||
state: crate::JobState,
|
||||
}
|
||||
|
||||
impl<S> JobNotification<S> {
|
||||
/// Creates one immutable latest-value notification from an already validated Job identity and snapshot.
|
||||
#[must_use]
|
||||
pub const fn new(id: crate::JobId, kind: crate::JobKindCode, sequence: crate::JobNotificationSequence, state: crate::JobState, snapshot: S) -> Self {
|
||||
return Self { id, kind, sequence, snapshot, state };
|
||||
}
|
||||
|
||||
/// Returns the logical Job identity.
|
||||
#[must_use]
|
||||
pub const fn id(&self) -> &crate::JobId {
|
||||
return &self.id;
|
||||
}
|
||||
|
||||
/// Returns the stable Job family code.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> &crate::JobKindCode {
|
||||
return &self.kind;
|
||||
}
|
||||
|
||||
/// Returns the monotone sequence of this latest value.
|
||||
#[must_use]
|
||||
pub const fn sequence(&self) -> crate::JobNotificationSequence {
|
||||
return self.sequence;
|
||||
}
|
||||
|
||||
/// Returns the complete safe snapshot owned by the concrete Job contract.
|
||||
#[must_use]
|
||||
pub const fn snapshot(&self) -> &S {
|
||||
return &self.snapshot;
|
||||
}
|
||||
|
||||
/// Returns the lifecycle state represented by this snapshot.
|
||||
#[must_use]
|
||||
pub const fn state(&self) -> crate::JobState {
|
||||
return self.state;
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> std::fmt::Debug for JobNotification<S> {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("JobNotification")
|
||||
.field("id", &self.id)
|
||||
.field("kind", &self.kind)
|
||||
.field("sequence", &self.sequence)
|
||||
.field("state", &self.state)
|
||||
.field("snapshot", &"<redacted>")
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime-neutral future returned while observing a latest-value Job snapshot source.
|
||||
pub type JobSnapshotFuture<'a, S> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = crate::JobNotification<S>> + std::marker::Send + 'a>>;
|
||||
|
||||
/// Runtime-neutral read and change-wait contract for one latest-value Job snapshot stream.
|
||||
pub trait JobSnapshotSource: std::marker::Send + std::marker::Sync {
|
||||
/// Complete snapshot type retained by the concrete source.
|
||||
type Snapshot: std::clone::Clone + std::marker::Send + std::marker::Sync + 'static;
|
||||
|
||||
/// Returns the complete current value without requiring replay of prior notifications.
|
||||
#[must_use]
|
||||
fn current(&self) -> crate::JobNotification<Self::Snapshot>;
|
||||
|
||||
/// Waits for a value newer than `observed`, returning the complete current snapshot after coalescing any intermediate updates.
|
||||
fn wait_for_change(&self, observed: crate::JobNotificationSequence) -> crate::JobSnapshotFuture<'_, Self::Snapshot>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn exhausted_notification_sequence() -> crate::JobNotificationSequence {
|
||||
return crate::JobNotificationSequence(u64::MAX);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/notification.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-api/tests/dependency_boundary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Dependency and runtime-neutrality canaries for the Job API foundation.
|
||||
|
||||
@@ -25,13 +25,14 @@ fn pre_002_manifest_has_exact_core_only_dependency_graph() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_production_sources_forbid_runtime_domain_and_wire_dependencies() {
|
||||
fn pre_003_production_sources_forbid_runtime_domain_and_wire_dependencies() {
|
||||
let sources = [
|
||||
include_str!("../src/cancellation.rs"),
|
||||
include_str!("../src/error.rs"),
|
||||
include_str!("../src/identity.rs"),
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/lifecycle.rs"),
|
||||
include_str!("../src/notification.rs"),
|
||||
];
|
||||
for source in sources {
|
||||
for forbidden in [
|
||||
|
||||
148
crates/ksp-job-api/tests/notifications.rs
Normal file
148
crates/ksp-job-api/tests/notifications.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
// file: crates/ksp-job-api/tests/notifications.rs
|
||||
// version: 1
|
||||
|
||||
//! External-consumer canaries for latest-value Job observation.
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct TestSnapshot {
|
||||
completed: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestSnapshotSource {
|
||||
current: std::sync::Arc<ksp_job_api::JobNotification<TestSnapshot>>,
|
||||
}
|
||||
|
||||
impl TestSnapshotSource {
|
||||
fn new(current: ksp_job_api::JobNotification<TestSnapshot>) -> Self {
|
||||
return Self { current: std::sync::Arc::new(current) };
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_job_api::JobSnapshotSource for TestSnapshotSource {
|
||||
type Snapshot = TestSnapshot;
|
||||
|
||||
fn current(&self) -> ksp_job_api::JobNotification<Self::Snapshot> {
|
||||
return self.current.as_ref().clone();
|
||||
}
|
||||
|
||||
fn wait_for_change(&self, observed: ksp_job_api::JobNotificationSequence) -> ksp_job_api::JobSnapshotFuture<'_, Self::Snapshot> {
|
||||
let current = ksp_job_api::JobSnapshotSource::current(self);
|
||||
assert!(current.sequence().is_after(observed));
|
||||
return std::boxed::Box::pin(std::future::ready(current));
|
||||
}
|
||||
}
|
||||
|
||||
struct TestWake;
|
||||
|
||||
impl std::task::Wake for TestWake {
|
||||
fn wake(self: std::sync::Arc<Self>) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_ready<S>(mut future: ksp_job_api::JobSnapshotFuture<'_, S>) -> std::option::Option<ksp_job_api::JobNotification<S>> {
|
||||
let waker = std::task::Waker::from(std::sync::Arc::new(TestWake));
|
||||
let mut context = std::task::Context::from_waker(&waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => std::option::Option::Some(value),
|
||||
std::task::Poll::Pending => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn notification_sequence(value: u64) -> std::option::Option<ksp_job_api::JobNotificationSequence> {
|
||||
let mut sequence = ksp_job_api::JobNotificationSequence::initial();
|
||||
for _ in 0..value {
|
||||
sequence = match sequence.next() {
|
||||
std::result::Result::Ok(next) => next,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
}
|
||||
return std::option::Option::Some(sequence);
|
||||
}
|
||||
|
||||
fn notification(sequence_value: u64, state: ksp_job_api::JobState, completed: u64) -> std::option::Option<ksp_job_api::JobNotification<TestSnapshot>> {
|
||||
let id = match ksp_job_api::JobId::new("external-job-001") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let kind = match ksp_job_api::JobKindCode::new("backfill_raw") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let sequence = match notification_sequence(sequence_value) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
return std::option::Option::Some(ksp_job_api::JobNotification::new(id, kind, sequence, state, TestSnapshot { completed }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_external_notification_contract_is_consumable_from_crate_root() {
|
||||
let notification = notification(3, ksp_job_api::JobState::Running, 2);
|
||||
assert!(notification.is_some());
|
||||
let notification = match notification {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(notification.id().as_str(), "external-job-001");
|
||||
assert_eq!(notification.kind().as_str(), "backfill_raw");
|
||||
assert_eq!(notification.sequence().value(), 3);
|
||||
assert_eq!(notification.state(), ksp_job_api::JobState::Running);
|
||||
assert_eq!(notification.snapshot().completed, 2);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_slow_and_independent_listeners_resynchronize_to_latest_value() {
|
||||
let latest = notification(8, ksp_job_api::JobState::Running, 7);
|
||||
assert!(latest.is_some());
|
||||
let source = match latest {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let listener_a = source.clone();
|
||||
let listener_b = source.clone();
|
||||
let observed_a = match notification_sequence(2) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let observed_b = match notification_sequence(6) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let changed_a = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_a, observed_a));
|
||||
let changed_b = poll_ready(ksp_job_api::JobSnapshotSource::wait_for_change(&listener_b, observed_b));
|
||||
assert!(changed_a.is_some());
|
||||
assert!(changed_b.is_some());
|
||||
let changed_a = match changed_a {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let changed_b = match changed_b {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(changed_a.sequence().value(), 8);
|
||||
assert_eq!(changed_b.sequence().value(), 8);
|
||||
assert_eq!(changed_a.snapshot().completed, 7);
|
||||
assert_eq!(changed_b.snapshot().completed, 7);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_terminal_snapshot_remains_readable_from_shared_source() {
|
||||
let terminal = notification(9, ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial), 8);
|
||||
assert!(terminal.is_some());
|
||||
let source = match terminal {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let cloned = source.clone();
|
||||
let current = ksp_job_api::JobSnapshotSource::current(&cloned);
|
||||
assert_eq!(current.sequence().value(), 9);
|
||||
assert_eq!(current.state(), ksp_job_api::JobState::Completed(ksp_job_api::JobCompletion::Partial));
|
||||
assert!(current.state().is_terminal());
|
||||
assert_eq!(current.snapshot().completed, 8);
|
||||
return;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: crates/ksp-job-api/tests/release_completeness.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Completeness canaries for the initial Job API foundation.
|
||||
//! Completeness canaries for the current Job API foundation.
|
||||
|
||||
#[test]
|
||||
fn pre_002_crate_root_export_inventory_is_exact() {
|
||||
fn pre_003_crate_root_export_inventory_is_exact() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let mut actual = std::vec::Vec::new();
|
||||
for line in crate_root.lines() {
|
||||
@@ -18,6 +18,7 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
||||
"pub use self::cancellation::JobCancellationToken;",
|
||||
"pub use self::error::ERROR_CODE_JOB_ID_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_JOB_KIND_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED;",
|
||||
"pub use self::error::ERROR_CODE_JOB_TRANSITION_INVALID;",
|
||||
"pub use self::identity::JobId;",
|
||||
"pub use self::identity::JobKindCode;",
|
||||
@@ -26,6 +27,10 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
||||
"pub use self::lifecycle::JobCompletion;",
|
||||
"pub use self::lifecycle::JobLifecycle;",
|
||||
"pub use self::lifecycle::JobState;",
|
||||
"pub use self::notification::JobNotification;",
|
||||
"pub use self::notification::JobNotificationSequence;",
|
||||
"pub use self::notification::JobSnapshotFuture;",
|
||||
"pub use self::notification::JobSnapshotSource;",
|
||||
"pub use ksp_core_lib::Error;",
|
||||
"pub use ksp_core_lib::ErrorCode;",
|
||||
"pub use ksp_core_lib::ErrorContext;",
|
||||
@@ -38,7 +43,7 @@ fn pre_002_crate_root_export_inventory_is_exact() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_production_module_inventory_is_exact() -> std::io::Result<()> {
|
||||
fn pre_003_production_module_inventory_is_exact() -> std::io::Result<()> {
|
||||
let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let entries = match std::fs::read_dir(source_root) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -66,35 +71,29 @@ fn pre_002_production_module_inventory_is_exact() -> std::io::Result<()> {
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, std::vec!["cancellation.rs", "error.rs", "identity.rs", "lib.rs", "lifecycle.rs"]);
|
||||
assert_eq!(names, std::vec!["cancellation.rs", "error.rs", "identity.rs", "lib.rs", "lifecycle.rs", "notification.rs"]);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_surface_does_not_open_notifications_backfill_or_worker_contracts() {
|
||||
fn pre_003_surface_opens_only_generic_notifications_without_backfill_or_worker_contracts() {
|
||||
let sources = [
|
||||
include_str!("../src/cancellation.rs"),
|
||||
include_str!("../src/identity.rs"),
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/lifecycle.rs"),
|
||||
include_str!("../src/notification.rs"),
|
||||
];
|
||||
for source in sources {
|
||||
for forbidden in [
|
||||
"JobNotification",
|
||||
"JobNotificationSequence",
|
||||
"JobSnapshot",
|
||||
"JobSnapshotSource",
|
||||
"BackfillRequest",
|
||||
"WorkerControl",
|
||||
"WorkerState",
|
||||
"spawn(",
|
||||
"RawTransaction",
|
||||
"provider",
|
||||
"endpoint",
|
||||
] {
|
||||
for forbidden in ["BackfillRequest", "WorkerControl", "WorkerState", "spawn(", "RawTransaction", "provider", "endpoint"] {
|
||||
assert!(!source.contains(forbidden), "future or domain-specific Job contract leaked early: {forbidden}");
|
||||
}
|
||||
}
|
||||
let notification_source = include_str!("../src/notification.rs");
|
||||
assert!(notification_source.contains("pub struct JobNotification<S>"));
|
||||
assert!(notification_source.contains("pub trait JobSnapshotSource"));
|
||||
assert!(notification_source.contains("std::future::Future"));
|
||||
assert!(!notification_source.contains("tokio::"));
|
||||
let lifecycle_source = include_str!("../src/lifecycle.rs");
|
||||
assert!(lifecycle_source.contains("#[derive(Eq, PartialEq)]\npub struct JobLifecycle"));
|
||||
assert!(!lifecycle_source.contains("#[derive(Clone, Eq, PartialEq)]\npub struct JobLifecycle"));
|
||||
|
||||
81
crates/ksp-job-api/unit_tests/notification.rs
Normal file
81
crates/ksp-job-api/unit_tests/notification.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// file: crates/ksp-job-api/unit_tests/notification.rs
|
||||
// version: 1
|
||||
|
||||
fn sequence(value: u64) -> crate::JobNotificationSequence {
|
||||
let mut sequence = crate::JobNotificationSequence::initial();
|
||||
for _ in 0..value {
|
||||
sequence = match sequence.next() {
|
||||
std::result::Result::Ok(next) => next,
|
||||
std::result::Result::Err(_) => return sequence,
|
||||
};
|
||||
}
|
||||
return sequence;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_notification_sequence_advances_strictly_and_orders_positions() {
|
||||
let mut first = crate::JobNotificationSequence::initial();
|
||||
for _ in 0..41 {
|
||||
let next = first.next();
|
||||
assert!(next.is_ok());
|
||||
first = match next {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
}
|
||||
let second = first.next();
|
||||
assert!(second.is_ok());
|
||||
let second = match second {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(first.value(), 41);
|
||||
assert_eq!(second.value(), 42);
|
||||
assert!(second.is_after(first));
|
||||
assert!(!first.is_after(second));
|
||||
assert!(second > first);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_notification_sequence_exhaustion_is_explicit_and_non_wrapping() {
|
||||
let exhausted = super::exhausted_notification_sequence().next();
|
||||
assert!(exhausted.is_err());
|
||||
let error = match exhausted {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JOB_NOTIFICATION_SEQUENCE_EXHAUSTED);
|
||||
assert_eq!(error.context().len(), 1);
|
||||
assert_eq!(error.context()[0].key(), "sequence");
|
||||
assert_eq!(error.context()[0].value(), u64::MAX.to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_notification_preserves_complete_value_and_redacts_snapshot_debug() {
|
||||
let id = crate::JobId::new("job-notify-001");
|
||||
let kind = crate::JobKindCode::new("backfill_raw");
|
||||
assert!(id.is_ok());
|
||||
assert!(kind.is_ok());
|
||||
let id = match id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let kind = match kind {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let notification =
|
||||
crate::JobNotification::new(id.clone(), kind.clone(), sequence(7), crate::JobState::Running, "RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG".to_string());
|
||||
assert_eq!(notification.id(), &id);
|
||||
assert_eq!(notification.kind(), &kind);
|
||||
assert_eq!(notification.sequence().value(), 7);
|
||||
assert_eq!(notification.state(), crate::JobState::Running);
|
||||
assert_eq!(notification.snapshot(), "RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG");
|
||||
let debug = std::format!("{notification:?}");
|
||||
assert!(debug.contains("JobNotification"));
|
||||
assert!(debug.contains("<redacted>"));
|
||||
assert!(!debug.contains("RAW-PAYLOAD-MUST-NOT-APPEAR-IN-DEBUG"));
|
||||
return;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-program-api/src/program_instruction_decode.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Recognition strength reported by one Program instruction implementation.
|
||||
///
|
||||
@@ -38,10 +38,10 @@ pub enum ProgramInstructionDecodeOutcome<Decoded> {
|
||||
|
||||
impl<Decoded> std::fmt::Debug for ProgramInstructionDecodeOutcome<Decoded> {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Decoded(_) => return formatter.write_str("Decoded"),
|
||||
Self::Unsupported => return formatter.write_str("Unsupported"),
|
||||
}
|
||||
return match self {
|
||||
Self::Decoded(_) => formatter.write_str("Decoded"),
|
||||
Self::Unsupported => formatter.write_str("Unsupported"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user