commit ae8d5a18416000c96498f460866ae3b1b45ab104
parent 15d8850412224d48073d20e09ae26bd96efeb4a4
Author: triesap <tyson@radroots.org>
Date: Sun, 16 Aug 2026 06:06:57 +0000
service-host: qualify stable support paths
Diffstat:
5 files changed, 69 insertions(+), 10 deletions(-)
diff --git a/crates/service_host/src/lifecycle/shutdown.rs b/crates/service_host/src/lifecycle/shutdown.rs
@@ -409,6 +409,7 @@ mod tests {
.unwrap();
let (mut handler, phases) = handler(None);
let mut shutdown = GracefulShutdown::new(Duration::from_secs(30)).unwrap();
+ assert_eq!(shutdown.grace(), Duration::from_secs(30));
let first = shutdown
.run(&clock(), &mut supervisor, &mut handler, pending())
@@ -520,6 +521,7 @@ mod tests {
);
assert!(!failure.to_string().contains("sensitive"));
assert!(!format!("{failure:?}").contains("sensitive shutdown detail"));
+ assert!(failure.source().is_some());
}
#[tokio::test]
@@ -553,9 +555,11 @@ mod tests {
#[test]
fn zero_grace_fails_closed() {
+ let error = GracefulShutdown::new(Duration::ZERO).err().unwrap();
+ assert_eq!(error, ShutdownConfigError::ZeroGrace);
assert_eq!(
- GracefulShutdown::new(Duration::ZERO).err(),
- Some(ShutdownConfigError::ZeroGrace)
+ error.to_string(),
+ "shutdown grace must be greater than zero"
);
}
@@ -569,14 +573,19 @@ mod tests {
let (mut handler, phases) = handler(None);
let mut shutdown = GracefulShutdown::new(Duration::from_nanos(1)).unwrap();
+ let error = shutdown
+ .run(&overflow_clock, &mut supervisor, &mut handler, pending())
+ .await
+ .unwrap_err();
+ assert_eq!(
+ error,
+ ShutdownStartError::Deadline(MonotonicClockError::DeadlineOverflow)
+ );
assert_eq!(
- shutdown
- .run(&overflow_clock, &mut supervisor, &mut handler, pending())
- .await,
- Err(ShutdownStartError::Deadline(
- MonotonicClockError::DeadlineOverflow
- ))
+ error.to_string(),
+ "shutdown grace deadline could not be represented"
);
+ assert!(error.source().is_some());
assert!(phases.lock().unwrap().is_empty());
assert!(!cancellation.is_cancelled());
assert!(shutdown.phase_failure().is_none());
diff --git a/crates/service_host/src/lifecycle/signal.rs b/crates/service_host/src/lifecycle/signal.rs
@@ -206,6 +206,10 @@ mod tests {
let mut before_first = ProcessSignalAdapter::new(InjectedSignals::new([]));
let error = before_first.next_action().await.unwrap_err();
assert_eq!(error.stage(), ProcessSignalStage::FirstCancellation);
+ assert_eq!(
+ error.to_string(),
+ "process signal source closed before the expected event"
+ );
assert_eq!(before_first.stage(), ProcessSignalStage::FirstCancellation);
let mut before_force =
@@ -225,8 +229,12 @@ mod tests {
#[test]
fn normalized_names_are_stable_and_platform_bounded() {
assert_eq!(ProcessSignal::Interrupt.as_str(), "interrupt");
+ assert_eq!(ProcessSignal::Interrupt.to_string(), "interrupt");
#[cfg(unix)]
- assert_eq!(ProcessSignal::Terminate.as_str(), "terminate");
+ {
+ assert_eq!(ProcessSignal::Terminate.as_str(), "terminate");
+ assert_eq!(ProcessSignal::Terminate.to_string(), "terminate");
+ }
let adapter = ProcessSignalAdapter::new(InjectedSignals::new([ProcessSignal::Interrupt]));
assert_eq!(adapter.into_inner().events.len(), 1);
diff --git a/crates/service_host/src/lifecycle/task.rs b/crates/service_host/src/lifecycle/task.rs
@@ -165,7 +165,9 @@ mod tests {
#[test]
fn task_names_are_bounded_stable_and_secret_safe() {
for valid in ["a", "admin_listener", "outbox_worker_01"] {
- assert_eq!(TaskName::new(valid).unwrap().as_str(), valid);
+ let name = TaskName::new(valid).unwrap();
+ assert_eq!(name.as_str(), valid);
+ assert_eq!(name.to_string(), valid);
}
assert!(TaskName::new("a".repeat(TASK_NAME_MAX_BYTES)).is_ok());
for invalid in [
@@ -188,6 +190,14 @@ mod tests {
TaskName::new("a".repeat(4 * 1024 * 1024)),
Err(TaskMetadataError::InvalidTaskName)
);
+ assert_eq!(
+ TaskMetadataError::InvalidTaskName.to_string(),
+ "supervised task name is invalid"
+ );
+ assert_eq!(
+ TaskMetadataError::InvalidShutdownPhaseAssignment.to_string(),
+ "supervised task shutdown phase does not match its classification"
+ );
}
#[test]
diff --git a/crates/service_host/src/status/reason.rs b/crates/service_host/src/status/reason.rs
@@ -276,6 +276,25 @@ mod tests {
}
#[test]
+ fn owned_text_traits_empty_state_and_deserializer_expectations_are_bound() {
+ let owned = ReasonCode::from_string("owned_reason".to_owned()).expect("owned reason");
+ assert_eq!(owned.to_string(), "owned_reason");
+ assert_eq!(
+ "parsed_reason".parse::<ReasonCode>().unwrap().as_str(),
+ "parsed_reason"
+ );
+ assert_eq!(
+ ReasonCode::from_string("Invalid".to_owned()),
+ Err(StatusContractError::InvalidReasonCode)
+ );
+
+ assert!(ReasonCodes::empty().is_empty());
+ assert!(!ReasonCodes::new([owned]).unwrap().is_empty());
+ assert!(serde_json::from_str::<ReasonCode>("42").is_err());
+ assert!(serde_json::from_str::<ReasonCodes>(r#"{"reason":"owned_reason"}"#).is_err());
+ }
+
+ #[test]
fn collections_sort_deduplicate_bound_and_serialize_canonically() {
let reasons = ReasonCodes::new([
ReasonCode::new("z_reason").unwrap(),
diff --git a/crates/service_host/src/time.rs b/crates/service_host/src/time.rs
@@ -251,5 +251,18 @@ mod tests {
let second = monotonic.now_monotonic();
assert!(second >= first);
assert!(monotonic.deadline_after(Duration::from_secs(1)).is_ok());
+
+ let default_monotonic = SystemMonotonicClock::default();
+ assert!(
+ default_monotonic.now_monotonic().duration_since_origin() <= Duration::from_secs(1)
+ );
+ assert_eq!(
+ WallClockError::BeforeUnixEpoch.to_string(),
+ "wall clock is before the Unix epoch"
+ );
+ assert_eq!(
+ MonotonicClockError::DeadlineOverflow.to_string(),
+ "monotonic deadline overflows"
+ );
}
}