v0.3.9-pre.003
This commit is contained in:
161
crates/ksp-worker-api/tests/release_completeness.rs
Normal file
161
crates/ksp-worker-api/tests/release_completeness.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: crates/ksp-worker-api/tests/release_completeness.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact frozen-surface and domain-firewall canaries for `ksp-worker-api`.
|
||||
|
||||
#[test]
|
||||
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() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("pub use ") {
|
||||
actual.push(trimmed);
|
||||
}
|
||||
}
|
||||
actual.sort_unstable();
|
||||
let mut expected = std::vec![
|
||||
"pub use self::error::ERROR_CODE_WORKER_ID_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_KIND_INVALID;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED;",
|
||||
"pub use self::error::ERROR_CODE_WORKER_TRANSITION_INVALID;",
|
||||
"pub use self::identity::MAX_WORKER_ID_BYTES;",
|
||||
"pub use self::identity::MAX_WORKER_KIND_CODE_BYTES;",
|
||||
"pub use self::identity::WorkerId;",
|
||||
"pub use self::identity::WorkerKindCode;",
|
||||
"pub use self::lifecycle::WorkerActivity;",
|
||||
"pub use self::lifecycle::WorkerHealth;",
|
||||
"pub use self::lifecycle::WorkerLifecycle;",
|
||||
"pub use self::lifecycle::WorkerState;",
|
||||
"pub use self::snapshot::WorkerSnapshot;",
|
||||
"pub use self::snapshot::WorkerSnapshotFuture;",
|
||||
"pub use self::snapshot::WorkerSnapshotSequence;",
|
||||
"pub use self::snapshot::WorkerSnapshotSource;",
|
||||
"pub use self::stop::WorkerStopToken;",
|
||||
"pub use ksp_core_lib::Error;",
|
||||
"pub use ksp_core_lib::ErrorCode;",
|
||||
"pub use ksp_core_lib::ErrorContext;",
|
||||
"pub use ksp_core_lib::Result;",
|
||||
];
|
||||
expected.sort_unstable();
|
||||
assert_eq!(actual, expected);
|
||||
assert!(!crate_root.contains("pub mod "));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
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,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut names = std::vec::Vec::new();
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = match entry.file_name().into_string() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
if name.ends_with(".rs") {
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
names.sort_unstable();
|
||||
assert_eq!(names, std::vec!["error.rs", "identity.rs", "lib.rs", "lifecycle.rs", "snapshot.rs", "stop.rs"]);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fixed_snapshot_shape_contains_only_common_worker_dimensions() {
|
||||
let snapshot = include_str!("../src/snapshot.rs");
|
||||
let struct_start = match snapshot.find("pub struct WorkerSnapshot {") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let after_start = &snapshot[struct_start..];
|
||||
let struct_end = match after_start.find("\n}") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let definition = &after_start[..struct_end];
|
||||
for required in ["id:", "kind:", "sequence:", "state:", "health:", "activity:"] {
|
||||
assert!(definition.contains(required), "missing frozen Worker snapshot field: {required}");
|
||||
}
|
||||
for forbidden in ["String", "Vec<", "payload", "slot", "signature", "provider", "endpoint", "checkpoint", "backfill", "transaction"] {
|
||||
assert!(!definition.contains(forbidden), "domain or arbitrary payload leaked into frozen Worker snapshot: {forbidden}");
|
||||
}
|
||||
let fields = definition.lines().filter(|line| {
|
||||
let line = line.trim_start();
|
||||
return line.starts_with("id:")
|
||||
|| line.starts_with("kind:")
|
||||
|| line.starts_with("sequence:")
|
||||
|| line.starts_with("state:")
|
||||
|| line.starts_with("health:")
|
||||
|| line.starts_with("activity:");
|
||||
});
|
||||
assert_eq!(fields.count(), 6);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_surface_remains_runtime_neutral_job_independent_and_domain_free() {
|
||||
let sources = [
|
||||
include_str!("../src/error.rs"),
|
||||
include_str!("../src/identity.rs"),
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/lifecycle.rs"),
|
||||
include_str!("../src/snapshot.rs"),
|
||||
include_str!("../src/stop.rs"),
|
||||
];
|
||||
for source in sources {
|
||||
for forbidden in [
|
||||
"Backfill",
|
||||
"RawTransaction",
|
||||
"WorkerHandle",
|
||||
"checkpoint",
|
||||
"endpoint",
|
||||
"futures::",
|
||||
"futures_util::",
|
||||
"ksp_config_lib::",
|
||||
"ksp_interface_lib::",
|
||||
"ksp_job_api::",
|
||||
"ksp_logging_lib::",
|
||||
"ksp_offchain_transport_lib::",
|
||||
"ksp_onchain_transport_lib::",
|
||||
"ksp_store_api::",
|
||||
"ksp_store_lib::",
|
||||
"provider",
|
||||
"reqwest::",
|
||||
"serde::",
|
||||
"serde_json::",
|
||||
"slot",
|
||||
"solana_",
|
||||
"tauri::",
|
||||
"tokio::",
|
||||
"tonic::",
|
||||
concat!("tracing", "::"),
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "forbidden frozen Worker API concern detected: {forbidden}");
|
||||
}
|
||||
}
|
||||
let snapshot = include_str!("../src/snapshot.rs");
|
||||
assert!(snapshot.contains("pub trait WorkerSnapshotSource: std::marker::Send + std::marker::Sync"));
|
||||
assert!(snapshot.contains("std::future::Future"));
|
||||
assert!(!snapshot.contains("std::sync::mpsc"));
|
||||
assert!(!snapshot.contains("VecDeque"));
|
||||
let lifecycle = include_str!("../src/lifecycle.rs");
|
||||
assert!(lifecycle.contains("#[derive(Eq, PartialEq)]\npub struct WorkerLifecycle"));
|
||||
assert!(!lifecycle.contains("#[derive(Clone, Eq, PartialEq)]\npub struct WorkerLifecycle"));
|
||||
return;
|
||||
}
|
||||
185
crates/ksp-worker-api/tests/security_hardening.rs
Normal file
185
crates/ksp-worker-api/tests/security_hardening.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
// file: crates/ksp-worker-api/tests/security_hardening.rs
|
||||
// version: 1
|
||||
|
||||
//! Adversarial lifecycle, stop and redaction canaries for the frozen Worker API.
|
||||
|
||||
const HOSTILE_MARKER: &str = "WORKER-IDENTITY-SECRET-CANARY";
|
||||
const TEST_FAULT: ksp_worker_api::ErrorCode = ksp_worker_api::ErrorCode::new("worker_test", "fault");
|
||||
|
||||
fn running_lifecycle(id_value: &str) -> std::option::Option<ksp_worker_api::WorkerLifecycle> {
|
||||
let id = match ksp_worker_api::WorkerId::new(id_value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("continuous_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
||||
if lifecycle.start().is_err() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
if lifecycle.mark_running().is_err() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return std::option::Option::Some(lifecycle);
|
||||
}
|
||||
|
||||
fn assert_all_mutators_reject(lifecycle: &mut ksp_worker_api::WorkerLifecycle, terminal: ksp_worker_api::WorkerState) {
|
||||
assert!(lifecycle.start().is_err());
|
||||
assert!(lifecycle.mark_running().is_err());
|
||||
assert!(lifecycle.mark_stopping().is_err());
|
||||
assert!(lifecycle.mark_stopped().is_err());
|
||||
assert!(lifecycle.fault(TEST_FAULT).is_err());
|
||||
assert_eq!(lifecycle.state(), terminal);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_worker_identity_lifecycle_and_snapshot_debug_redact_hostile_identity() {
|
||||
let lifecycle = running_lifecycle(HOSTILE_MARKER);
|
||||
assert!(lifecycle.is_some());
|
||||
let lifecycle = match lifecycle {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let id = lifecycle.id();
|
||||
assert_eq!(std::format!("{id:?}"), "WorkerId(..)");
|
||||
let lifecycle_debug = std::format!("{lifecycle:?}");
|
||||
assert!(lifecycle_debug.contains("WorkerLifecycle"));
|
||||
assert!(lifecycle_debug.contains("continuous_worker"));
|
||||
assert!(lifecycle_debug.contains("Running"));
|
||||
assert!(!lifecycle_debug.contains(HOSTILE_MARKER));
|
||||
let snapshot = ksp_worker_api::WorkerSnapshot::new(
|
||||
lifecycle.id().clone(),
|
||||
lifecycle.kind().clone(),
|
||||
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
||||
lifecycle.state(),
|
||||
ksp_worker_api::WorkerHealth::Healthy,
|
||||
ksp_worker_api::WorkerActivity::Active,
|
||||
);
|
||||
let snapshot_debug = std::format!("{snapshot:?}");
|
||||
assert!(snapshot_debug.contains("WorkerSnapshot"));
|
||||
assert!(snapshot_debug.contains("continuous_worker"));
|
||||
assert!(snapshot_debug.contains("Healthy"));
|
||||
assert!(snapshot_debug.contains("Active"));
|
||||
assert!(!snapshot_debug.contains(HOSTILE_MARKER));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_identity_errors_never_echo_hostile_values() {
|
||||
let hostile = "WORKER-SECRET/CANARY";
|
||||
let rejected = ksp_worker_api::WorkerId::new(hostile);
|
||||
assert!(rejected.is_err());
|
||||
let error = match rejected {
|
||||
std::result::Result::Err(value) => value,
|
||||
std::result::Result::Ok(_) => return,
|
||||
};
|
||||
assert_eq!(error.code(), ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID);
|
||||
assert!(!std::format!("{error}").contains(hostile));
|
||||
assert!(!std::format!("{error:?}").contains(hostile));
|
||||
for context in error.context() {
|
||||
assert!(!context.value().contains(hostile));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stopped_and_faulted_are_immutable_under_all_public_mutators() {
|
||||
let id = match ksp_worker_api::WorkerId::new("terminal-created") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("terminal_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut stopped = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
||||
assert!(stopped.mark_stopped().is_ok());
|
||||
assert_all_mutators_reject(&mut stopped, ksp_worker_api::WorkerState::Stopped);
|
||||
let faulted = running_lifecycle("terminal-faulted");
|
||||
assert!(faulted.is_some());
|
||||
let mut faulted = match faulted {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(faulted.fault(TEST_FAULT).is_ok());
|
||||
assert_all_mutators_reject(&mut faulted, ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stop_fault_terminal_orders_are_first_valid_terminal_wins() {
|
||||
let direct_fault = running_lifecycle("race-direct-fault");
|
||||
assert!(direct_fault.is_some());
|
||||
let mut direct_fault = match direct_fault {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(direct_fault.fault(TEST_FAULT).is_ok());
|
||||
assert!(direct_fault.mark_stopping().is_err());
|
||||
assert_eq!(direct_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
let stopping_fault = running_lifecycle("race-stopping-fault");
|
||||
assert!(stopping_fault.is_some());
|
||||
let mut stopping_fault = match stopping_fault {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(stopping_fault.mark_stopping().is_ok());
|
||||
assert!(stopping_fault.fault(TEST_FAULT).is_ok());
|
||||
assert!(stopping_fault.mark_stopped().is_err());
|
||||
assert_eq!(stopping_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
let stopping_stopped = running_lifecycle("race-stopping-stopped");
|
||||
assert!(stopping_stopped.is_some());
|
||||
let mut stopping_stopped = match stopping_stopped {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(stopping_stopped.mark_stopping().is_ok());
|
||||
assert!(stopping_stopped.mark_stopped().is_ok());
|
||||
assert!(stopping_stopped.fault(TEST_FAULT).is_err());
|
||||
assert_eq!(stopping_stopped.state(), ksp_worker_api::WorkerState::Stopped);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_stop_token_is_send_sync_cross_thread_and_cannot_reanimate_terminal_lifecycle() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
||||
let lifecycle = running_lifecycle("cross-thread-stop");
|
||||
assert!(lifecycle.is_some());
|
||||
let mut lifecycle = match lifecycle {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(lifecycle.fault(TEST_FAULT).is_ok());
|
||||
let token = ksp_worker_api::WorkerStopToken::new();
|
||||
let worker_token = token.clone();
|
||||
let thread = std::thread::spawn(move || return worker_token.request_stop());
|
||||
let first = match thread.join() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(first);
|
||||
assert!(token.is_stop_requested());
|
||||
assert!(!token.request_stop());
|
||||
assert!(lifecycle.mark_stopping().is_err());
|
||||
assert_eq!(lifecycle.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_public_worker_primitives_keep_promised_send_sync_contracts() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<ksp_worker_api::WorkerId>();
|
||||
require_send_sync::<ksp_worker_api::WorkerKindCode>();
|
||||
require_send_sync::<ksp_worker_api::WorkerState>();
|
||||
require_send_sync::<ksp_worker_api::WorkerHealth>();
|
||||
require_send_sync::<ksp_worker_api::WorkerActivity>();
|
||||
require_send_sync::<ksp_worker_api::WorkerLifecycle>();
|
||||
require_send_sync::<ksp_worker_api::WorkerSnapshotSequence>();
|
||||
require_send_sync::<ksp_worker_api::WorkerSnapshot>();
|
||||
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
||||
return;
|
||||
}
|
||||
219
crates/ksp-worker-api/tests/snapshot_source.rs
Normal file
219
crates/ksp-worker-api/tests/snapshot_source.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
// file: crates/ksp-worker-api/tests/snapshot_source.rs
|
||||
// version: 1
|
||||
|
||||
//! External std-only latest-value source, object-safety and resynchronization canaries.
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestSnapshotSource {
|
||||
state: std::sync::Arc<TestState>,
|
||||
}
|
||||
|
||||
struct TestState {
|
||||
current: std::sync::Mutex<TestInner>,
|
||||
}
|
||||
|
||||
struct TestInner {
|
||||
snapshot: ksp_worker_api::WorkerSnapshot,
|
||||
waiters: std::vec::Vec<std::task::Waker>,
|
||||
}
|
||||
|
||||
impl TestSnapshotSource {
|
||||
fn new(snapshot: ksp_worker_api::WorkerSnapshot) -> Self {
|
||||
return Self {
|
||||
state: std::sync::Arc::new(TestState { current: std::sync::Mutex::new(TestInner { snapshot, waiters: std::vec::Vec::new() }) }),
|
||||
};
|
||||
}
|
||||
|
||||
fn publish(&self, state: ksp_worker_api::WorkerState, health: ksp_worker_api::WorkerHealth, activity: ksp_worker_api::WorkerActivity) -> bool {
|
||||
let mut current = match self.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
let sequence = match current.snapshot.sequence().next() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let id = current.snapshot.id().clone();
|
||||
let kind = current.snapshot.kind().clone();
|
||||
current.snapshot = ksp_worker_api::WorkerSnapshot::new(id, kind, sequence, state, health, activity);
|
||||
let waiters = std::mem::take(&mut current.waiters);
|
||||
std::mem::drop(current);
|
||||
for waiter in waiters {
|
||||
waiter.wake();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
struct TestWaitFuture<'a> {
|
||||
source: &'a TestSnapshotSource,
|
||||
observed: ksp_worker_api::WorkerSnapshotSequence,
|
||||
}
|
||||
|
||||
impl std::future::Future for TestWaitFuture<'_> {
|
||||
type Output = ksp_worker_api::WorkerSnapshot;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, context: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
|
||||
let mut current = match self.source.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
if current.snapshot.sequence().is_after(self.observed) {
|
||||
return std::task::Poll::Ready(current.snapshot.clone());
|
||||
}
|
||||
if !current.waiters.iter().any(|registered| return registered.will_wake(context.waker())) {
|
||||
current.waiters.push(context.waker().clone());
|
||||
}
|
||||
return std::task::Poll::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
impl ksp_worker_api::WorkerSnapshotSource for TestSnapshotSource {
|
||||
fn current(&self) -> ksp_worker_api::WorkerSnapshot {
|
||||
let current = match self.state.current.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
return current.snapshot.clone();
|
||||
}
|
||||
|
||||
fn wait_for_change(&self, observed: ksp_worker_api::WorkerSnapshotSequence) -> ksp_worker_api::WorkerSnapshotFuture<'_> {
|
||||
return std::boxed::Box::pin(TestWaitFuture { source: self, observed });
|
||||
}
|
||||
}
|
||||
|
||||
struct WakeProbe {
|
||||
woken: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl WakeProbe {
|
||||
fn new() -> Self {
|
||||
return Self { woken: std::sync::atomic::AtomicBool::new(false) };
|
||||
}
|
||||
|
||||
fn is_woken(&self) -> bool {
|
||||
return self.woken.load(std::sync::atomic::Ordering::Acquire);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::task::Wake for WakeProbe {
|
||||
fn wake(self: std::sync::Arc<Self>) {
|
||||
self.woken.store(true, std::sync::atomic::Ordering::Release);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_snapshot(future: &mut ksp_worker_api::WorkerSnapshotFuture<'_>, wake: &std::sync::Arc<WakeProbe>) -> std::task::Poll<ksp_worker_api::WorkerSnapshot> {
|
||||
let waker = std::task::Waker::from(wake.clone());
|
||||
let mut context = std::task::Context::from_waker(&waker);
|
||||
return std::future::Future::poll(future.as_mut(), &mut context);
|
||||
}
|
||||
|
||||
fn initial_snapshot() -> std::option::Option<ksp_worker_api::WorkerSnapshot> {
|
||||
let id = match ksp_worker_api::WorkerId::new("external-source-001") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new("external_test_worker") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return std::option::Option::Some(ksp_worker_api::WorkerSnapshot::new(
|
||||
id,
|
||||
kind,
|
||||
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
||||
ksp_worker_api::WorkerState::Running,
|
||||
ksp_worker_api::WorkerHealth::Healthy,
|
||||
ksp_worker_api::WorkerActivity::Idle,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_snapshot_source_is_object_safe_send_sync_and_externally_implementable() {
|
||||
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
||||
require_send_sync::<TestSnapshotSource>();
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let object: &dyn ksp_worker_api::WorkerSnapshotSource = &source;
|
||||
let current = object.current();
|
||||
assert_eq!(current.sequence().value(), 0);
|
||||
let mut wait = object.wait_for_change(current.sequence());
|
||||
fn require_send<T: std::marker::Send>(_: &T) {}
|
||||
require_send(&wait);
|
||||
let wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut wait, &wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(wake.is_woken());
|
||||
let changed = poll_snapshot(&mut wait, &wake);
|
||||
assert!(matches!(changed, std::task::Poll::Ready(_)));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_slow_and_independent_listeners_coalesce_to_latest_value() {
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let observed = ksp_worker_api::WorkerSnapshotSource::current(&source).sequence();
|
||||
let mut slow = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, observed);
|
||||
let mut fast = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, observed);
|
||||
let slow_wake = std::sync::Arc::new(WakeProbe::new());
|
||||
let fast_wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut slow, &slow_wake), std::task::Poll::Pending));
|
||||
assert!(matches!(poll_snapshot(&mut fast, &fast_wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Degraded, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(slow_wake.is_woken());
|
||||
assert!(fast_wake.is_woken());
|
||||
let fast_value = match poll_snapshot(&mut fast, &fast_wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(fast_value.sequence().value(), 1);
|
||||
assert_eq!(fast_value.health(), ksp_worker_api::WorkerHealth::Degraded);
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
let slow_value = match poll_snapshot(&mut slow, &slow_wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(slow_value.sequence().value(), 2);
|
||||
assert_eq!(slow_value.health(), ksp_worker_api::WorkerHealth::Healthy);
|
||||
assert_eq!(slow_value.activity(), ksp_worker_api::WorkerActivity::Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_late_listener_resynchronizes_and_terminal_snapshot_remains_current() {
|
||||
let snapshot = initial_snapshot();
|
||||
assert!(snapshot.is_some());
|
||||
let source = match snapshot {
|
||||
std::option::Option::Some(value) => TestSnapshotSource::new(value),
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Running, ksp_worker_api::WorkerHealth::Degraded, ksp_worker_api::WorkerActivity::Active));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Stopping, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
let current = ksp_worker_api::WorkerSnapshotSource::current(&source);
|
||||
assert_eq!(current.sequence().value(), 2);
|
||||
assert_eq!(current.state(), ksp_worker_api::WorkerState::Stopping);
|
||||
let mut wait = ksp_worker_api::WorkerSnapshotSource::wait_for_change(&source, current.sequence());
|
||||
let wake = std::sync::Arc::new(WakeProbe::new());
|
||||
assert!(matches!(poll_snapshot(&mut wait, &wake), std::task::Poll::Pending));
|
||||
assert!(source.publish(ksp_worker_api::WorkerState::Stopped, ksp_worker_api::WorkerHealth::Healthy, ksp_worker_api::WorkerActivity::Idle));
|
||||
assert!(wake.is_woken());
|
||||
let terminal = match poll_snapshot(&mut wait, &wake) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => return,
|
||||
};
|
||||
assert_eq!(terminal.sequence().value(), 3);
|
||||
assert_eq!(terminal.state(), ksp_worker_api::WorkerState::Stopped);
|
||||
assert!(terminal.state().is_terminal());
|
||||
let retained = ksp_worker_api::WorkerSnapshotSource::current(&source);
|
||||
assert_eq!(retained, terminal);
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user