v0.3.6-pre.003

This commit is contained in:
2026-09-01 11:58:38 +02:00
parent cb670952e8
commit f92c22d55a
12 changed files with 604 additions and 48 deletions

View File

@@ -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 [

View 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;
}

View File

@@ -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"));