From 8d2d2daa7c832f7305235f26a11ebc09cda384dc Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:42:59 +0200 Subject: [PATCH] Hold 0115's creation, its capability answer and the core's lifetime (#115) 0115 decides what a client hands to creation, that creation reaches nothing, that a stop is bounded and reports which lane did not stop, that a stopped core is finished rather than restartable, and that a suspend keeps what a stop discards. None of it was anywhere in the tree: `Core` was still the placeholder this crate uses for a type whose issue has not been worked. src/lifecycle/mod.rs holds the part a value settles. `Supplied` is the whole of what creation takes and holds borrowed implementations and nothing else, so there is no state a creation could have filled in and no call it could have made. `WhatIsPresent` answers per implementation rather than as one flag, because three absences produce three differently reduced cores and a client that cannot tell them apart cannot explain any of them to an operator. `Lifetime` answers what a call does now, and it asks whether a stop was ASKED FOR rather than whether it succeeded, which is 0115 in its own words. The failure this prevents is the direction 0115 says three of its four answers go wrong in when a call site meets them first, which is towards making a call look like it succeeded. A stop that reports success after a timeout is a negative disclosure turned positive. A core that goes on taking work after a stop expired is one accepting requests while a lane it could not stop is still running. A second stop overwriting the first outcome is the same thing arriving by repetition, so the outcome is kept rather than replaced. The floor 0115 requires under the stop bound is NOT here, and that is a gap rather than a decision. It is fixed by how long the two uninterruptible things in 0009 take, neither record states either number, and a floor written here would be that number invented at the one call site needing it. What is refused is a bound of nothing, which is a stop that never waits for a lane at all. Nothing here starts, cancels or waits for a thread, for the same reason src/server/transport.rs holds no socket. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- .github/coverage/pinned-surface | 2 + src/lib.rs | 16 +- src/lifecycle/mod.rs | 700 ++++++++++++++++++++++++++++++++ tests/thread_statements.rs | 14 + 4 files changed, 731 insertions(+), 1 deletion(-) create mode 100644 src/lifecycle/mod.rs diff --git a/.github/coverage/pinned-surface b/.github/coverage/pinned-surface index c6e6a46..4711070 100644 --- a/.github/coverage/pinned-surface +++ b/.github/coverage/pinned-surface @@ -66,6 +66,7 @@ area src/server/ Reaching a server: the address a person typed, which hos area src/cache/ What is written to a client's storage and under what key. 0041 makes the key what stops two servers and two people on one device reading each other's entries, and everything that decides what the store is asked to hold is in the same directory. area src/failure/ The one point every failure in the core is mapped onto the vocabulary in 0004. A defect here is a case falling through and a caller being told nothing went wrong. area src/diagnostics/ What may leave the device through an event. 0071 decides that per field name and before the client's sink is called, so a defect here is a token, an account or an item identifier in a log somebody is asked to attach to a bug report. +area src/lifecycle/ The core's own lifetime: what a client hands to creation, which of those the core has, the bound a stop is given, and what a call does once a stop has been asked for. A defect here reports a stop as done while a lane it could not stop is still running, or lets a core that was asked to stop keep taking work, and 0115 puts both on the list of answers that go wrong towards making a call look like it succeeded. area src/artwork/ Turning bytes that arrived over a network into pixels, which 0003 calls the most attacked surface this repository will carry, and the refusal of every format the core does not decode. module src/session/mod.rs The session handle and the secret store interface a client implements. @@ -75,6 +76,7 @@ module src/session/password.rs 0030's password: the one reading that spend module src/session/quick_connect.rs 0031's cadence for asking about a Quick Connect exchange, its four endings, and which of the two values the server issued crosses to the client. A defect here is a backoff nobody decided on a route where the answer arrives from a person, a denial reported as a failure, or the value the core presents handed out beside the code. module src/session/sign_out.rs 0114's two acts: what each of them takes away, the order that puts the local half of a sign-out before the request to the server, how work in flight ends, and what a removal that could not be completed reports. A defect here leaves a token in memory on a device somebody handed over, empties a library on an ordinary evening act, or tells an operator their data is gone when it is not. module src/session/device.rs The device identity, whose identifier is one part of the cache key 0041 derives and one half of the key a server puts a live session under, so a defect here reaches two other surfaces on this list. +module src/lifecycle/mod.rs 0115's creation, its capability answer, its stop bound and the lifetime a call is judged against. module src/server/mod.rs The server module and what it places at its own boundary. module src/server/address.rs The address a person typed, parsed and refused where it is not usable. module src/server/certificate.rs The one exception 0029 admits to certificate validation: which certificate an operator pinned for which server, and what a pin never vouches for. A defect here accepts a certificate nobody asserted was theirs, or turns one server's key into something that can answer for a name the person never typed. diff --git a/src/lib.rs b/src/lib.rs index db9f396..681c0fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,13 @@ //! the one source all three clocks reach the core through, for the same reason: //! `docs/decisions/0102-the-clocks-every-deadline-is-measured-against.md` states //! a rule per clock, and a reading taken anywhere else would be a deadline no -//! test can move. +//! test can move. [`lifecycle`] holds what +//! `docs/decisions/0115-creating-and-stopping-the-core.md` decides about the +//! core's own lifetime: what a client hands to creation, which of those the core +//! has, the bound a stop is given, which lane a stop that expired names, and +//! what a call does once a stop has been asked for. It is beside [`Core`] rather +//! than inside one of the six because its subject is the core itself, and it +//! starts no thread, for the same reason `server::transport` holds no socket. //! //! # What is deliberately not here //! @@ -86,6 +92,7 @@ pub mod cache; pub mod clock; pub mod diagnostics; pub mod failure; +pub mod lifecycle; pub mod measurement; pub mod playback; pub mod server; @@ -112,6 +119,13 @@ const fn any_thread() {} const _: () = { any_thread::(); + any_thread::(); + any_thread::(); + any_thread::(); + any_thread::(); + any_thread::>(); + any_thread::(); + any_thread::(); any_thread::(); any_thread::(); any_thread::(); diff --git a/src/lifecycle/mod.rs b/src/lifecycle/mod.rs new file mode 100644 index 0000000..1b86cf9 --- /dev/null +++ b/src/lifecycle/mod.rs @@ -0,0 +1,700 @@ +//! Creating the core, stopping it, and a host that suspends it. +//! +//! `docs/decisions/0115-creating-and-stopping-the-core.md` decides all three and +//! `docs/decisions/0009-the-concurrency-model.md` carries the shape underneath +//! them. What is here is the part of both that a value settles: what a client +//! hands to creation, which of those the core has, the bound a stop is given, +//! which lane a stop that expired names, and what a call does once a stop has +//! been asked for. +//! +//! # What is here, and what is deliberately not +//! +//! WHAT IS NOT HERE IS A LANE. 0009 creates two threads when the core is created +//! and a stop is the one call in the core that waits on one. Nothing in this tree +//! starts a thread, so nothing here starts, cancels or waits for anything: this +//! module holds what the answers are, and the day something runs, it is what the +//! running code is judged against. That is the same position +//! [`crate::server::transport`] takes about the socket it does not hold. +//! +//! WHAT IS NOT HERE IS THE FLOOR UNDER THE STOP BOUND, and its absence is a gap +//! rather than a decision. 0115 requires one and says it is not a preference, +//! because 0009 fixes two things the core cannot interrupt - a decode running to +//! the end of its current step, and a read already begun through the byte store. +//! Neither record states how long either of those may take, and no other record +//! in this tree does, so a floor written here would be a number this repository +//! invented at the one call site that needed it. [`StopBound`] carries the +//! default and no floor, and says so where somebody setting one meets it. +//! +//! # Why creation reaching nothing is a property rather than a description +//! +//! 0115 refuses a creation call that restores a session, opens a connection, +//! resolves a name, reads an entry or reads a secret. [`Supplied`] is the whole +//! of what creation takes and it holds borrowed implementations and nothing else, +//! so there is no state for a creation to have filled in and no call for one to +//! have made. [`Supplied::what_is_present`] reads the three it was handed and +//! asks none of them anything. + +use core::time::Duration; + +use crate::cache::ByteStore; +use crate::diagnostics::DiagnosticsSink; +use crate::failure::Failure; +use crate::session::SecretStore; + +/// The two seconds a stop is bounded by where a client sets nothing. +/// +/// From 0115. Two is chosen so that a stop called from inside a platform's own +/// termination callback returns while that callback still has time left. The +/// windows those platforms allow are a claim in that record rather than a +/// measurement: nothing in this tree runs on a platform, and no command in this +/// repository produces them. +/// +/// A client that knows its own window sets the bound rather than accepting a +/// default chosen against a claim. +pub const A_STOP_IS_BOUNDED_AT: Duration = Duration::from_secs(2); + +/// One of the two threads 0009 creates with the core. +/// +/// The set is closed and it is 0009's rather than one invented here. A stop that +/// expired reports which of them did not stop, because "the core could not stop" +/// tells whoever reads it nothing they can act on, and the two lanes carry +/// different work for different reasons. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Lane { + /// Everything waiting on a server. + Waiting, + /// Everything that costs a processor, which is where a decode runs. + Processing, +} + +impl Lane { + /// The name this lane is reported under. + #[must_use] + pub const fn declared_name(self) -> &'static str { + match self { + Self::Waiting => "waiting", + Self::Processing => "processing", + } + } +} + +/// How a stop ended. +/// +/// 0009 refuses reporting an expiry as a stop, and this is that refusal as a +/// type: there is no variant that says a stop succeeded without saying that both +/// lanes stopped, and the expiry carries the lane rather than being a bare +/// failure. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HowTheStopEnded { + /// Both lanes stopped inside the bound. + BothLanesStopped, + /// The bound expired with this lane still running. + ALaneDidNotStop(Lane), +} + +/// The bound a stop is given. +/// +/// THERE IS NO FLOOR HERE AND 0115 ASKS FOR ONE. That record says a bound below +/// what 0009 makes uninterruptible produces a stop that always reports failure, +/// which teaches a client to ignore the report, and it says the floor is not a +/// preference. Neither 0009 nor any other record in this tree says how long a +/// decode step or a byte-store read may take, so the number that floor would be +/// does not exist yet, and inventing one here would fix it in the place least +/// likely to be argued with. What is refused instead is a bound of nothing, +/// which is a stop that cannot wait for a lane at all. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StopBound { + within: Duration, +} + +impl Default for StopBound { + fn default() -> Self { + Self::the_default() + } +} + +impl StopBound { + /// The bound a client that sets nothing gets. + #[must_use] + pub const fn the_default() -> Self { + Self { + within: A_STOP_IS_BOUNDED_AT, + } + } + + /// The bound a client set. + /// + /// A zero bound is refused rather than accepted, because 0009 makes a stop + /// wait for both lanes and a bound of nothing is a stop that never waits, + /// which reports an expiry on a core that would have stopped. Every other + /// value is taken, and the paragraph on this type says why no floor above it + /// is applied. + #[must_use] + pub const fn of(within: Duration) -> Option { + if within.is_zero() { + return None; + } + Some(Self { within }) + } + + /// How long a stop may wait for the lanes. + #[must_use] + pub const fn within(self) -> Duration { + self.within + } +} + +/// Which of the three implementations a client handed to creation are present. +/// +/// 0115 asks for this because three separate absences produce a core that works +/// in three different reduced ways, and a client that cannot ask is a client that +/// cannot explain any of them to an operator. What each absence costs is decided +/// in 0033, 0040 and 0100, one each, and is not repeated here. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. 0115 makes the +/// call that answers it one that cannot wait. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WhatIsPresent { + byte_store: bool, + secret_store: bool, + diagnostics_sink: bool, +} + +impl WhatIsPresent { + /// Whether the byte store from 0040 was supplied. + #[must_use] + pub const fn byte_store(self) -> bool { + self.byte_store + } + + /// Whether the secret store from 0033 was supplied. + #[must_use] + pub const fn secret_store(self) -> bool { + self.secret_store + } + + /// Whether the diagnostics sink from 0100 was supplied. + #[must_use] + pub const fn diagnostics_sink(self) -> bool { + self.diagnostics_sink + } + + /// Whether a client supplied none of the three. + /// + /// This is the core 0115's first condition is about, and it is legal rather + /// than degraded: each absence has a record saying what the core does + /// instead. + #[must_use] + pub const fn nothing_was_supplied(self) -> bool { + !self.byte_store && !self.secret_store && !self.diagnostics_sink + } +} + +/// Everything the core wants from a client, as creation takes it. +/// +/// One value rather than three arguments, so that adding a fourth +/// implementation is a method here rather than a change to every call site, and +/// so that [`Supplied::what_is_present`] is derived from the same thing creation +/// was handed rather than from a second record of it. +/// +/// Each may be absent. A [`Supplied::nothing`] is the whole of 0115's "no client +/// implementations supplied at all", and it is a legal core. +/// +/// Thread safety, from 0009: safe from any thread. Each of the three interfaces +/// requires it of the client's own implementation, so this value carries the +/// same bound rather than weakening it. +#[derive(Clone, Copy)] +pub struct Supplied<'a> { + byte_store: Option<&'a dyn ByteStore>, + secret_store: Option<&'a dyn SecretStore>, + diagnostics_sink: Option<&'a dyn DiagnosticsSink>, +} + +impl core::fmt::Debug for Supplied<'_> { + /// Says which of the three are present and nothing about any of them. + /// + /// The implementations are the client's own and this core has no words for + /// them, so what is written is the answer [`Supplied::what_is_present`] + /// gives. A derived one would need each interface to carry a formatting of + /// its own, which is a demand on a client's type for the benefit of a line + /// nobody reads. + fn fmt(&self, out: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + out.debug_struct("Supplied") + .field("byte_store", &self.byte_store.is_some()) + .field("secret_store", &self.secret_store.is_some()) + .field("diagnostics_sink", &self.diagnostics_sink.is_some()) + .finish() + } +} + +impl Default for Supplied<'_> { + fn default() -> Self { + Self::nothing() + } +} + +impl<'a> Supplied<'a> { + /// A client supplying none of the three. + #[must_use] + pub const fn nothing() -> Self { + Self { + byte_store: None, + secret_store: None, + diagnostics_sink: None, + } + } + + /// With the byte store 0040 defines. + #[must_use] + pub const fn and_the_byte_store(mut self, store: &'a dyn ByteStore) -> Self { + self.byte_store = Some(store); + self + } + + /// With the secret store 0033 defines. + #[must_use] + pub const fn and_the_secret_store(mut self, store: &'a dyn SecretStore) -> Self { + self.secret_store = Some(store); + self + } + + /// With the diagnostics sink 0100 defines. + #[must_use] + pub const fn and_the_diagnostics_sink(mut self, sink: &'a dyn DiagnosticsSink) -> Self { + self.diagnostics_sink = Some(sink); + self + } + + /// The byte store, where one was supplied. + #[must_use] + pub const fn byte_store(&self) -> Option<&'a dyn ByteStore> { + self.byte_store + } + + /// The secret store, where one was supplied. + #[must_use] + pub const fn secret_store(&self) -> Option<&'a dyn SecretStore> { + self.secret_store + } + + /// The diagnostics sink, where one was supplied. + #[must_use] + pub const fn diagnostics_sink(&self) -> Option<&'a dyn DiagnosticsSink> { + self.diagnostics_sink + } + + /// Which of the three are present. + /// + /// It reads what it was handed and asks none of them anything, which is + /// 0115's "creation reaches nothing" holding for the capability answer too: + /// a call that probed a store to find out whether it worked would be a + /// creation-time store read under another name. + #[must_use] + pub const fn what_is_present(&self) -> WhatIsPresent { + WhatIsPresent { + byte_store: self.byte_store.is_some(), + secret_store: self.secret_store.is_some(), + diagnostics_sink: self.diagnostics_sink.is_some(), + } + } +} + +/// What a call made against the core does now. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WhatACallDoes { + /// The core has not been asked to stop, so the call is made. + /// + /// A suspended core is here rather than beside it. 0115 says a suspend + /// cancels nothing the caller still wants, because the host has not asked + /// for that. + GoesAhead, + /// A stop was asked for, so the call fails without being made. + /// + /// `cancelled` is an imperfect fit and 0115 says so rather than growing the + /// vocabulary: its meaning in 0004 is that the caller asked for this to stop. + /// What makes the reuse tolerable is that nothing but the client's own stop + /// puts a core into this state. + FailsWith(Failure), +} + +/// Where the core is in the lifetime 0115 fixes. +/// +/// Four positions and three moves. A core is running when it is created; a +/// suspend and a resume move between running and suspended and keep everything; +/// a stop moves to finished from either and keeps nothing. +/// +/// THERE IS NO MOVE BACK OUT OF FINISHED, and that is 0115 rather than an +/// omission. A restartable core needs a rule for what survives a stop, per piece +/// of state, and that record's own list of what would need one is long: the +/// lanes, the capability answers, the correlator salt in 0071, whatever the +/// transport was holding. Creating a second core is the answer. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. What the +/// running core does with it is #115's remaining conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Lifetime { + suspended: bool, + stopped: Option, +} + +impl Default for Lifetime { + fn default() -> Self { + Self::created() + } +} + +impl Lifetime { + /// A core that has just been created. + /// + /// Running, not suspended and not stopped. 0115 says a core that has just + /// been created has started its two lanes and has done nothing else. + #[must_use] + pub const fn created() -> Self { + Self { + suspended: false, + stopped: None, + } + } + + /// The host is setting the process aside. + /// + /// It stops the core's own scheduled work, which is 0045's recovery schedule + /// and 0057's reporting cadence. It cancels nothing and flushes nothing. + /// + /// A suspend after a stop was asked for changes nothing. The core is + /// finished, and a host setting aside a process whose core has stopped is + /// not a reason to unsay that. + #[must_use] + pub const fn suspended(mut self) -> Self { + if self.stopped.is_some() { + return self; + } + self.suspended = true; + self + } + + /// The host has brought the process back. + /// + /// It restarts what a suspend stopped and does nothing else. 0115 refuses a + /// resume that refreshes, revalidates the cache or renews a token ahead of a + /// call, because each of those spends a person's connection at the moment + /// they have picked the device up and are waiting to see something. + /// + /// What a resume may not assume is written in 0115 and reaches other + /// modules: the token is neither trusted nor refused here, and every + /// connection the transport was holding is discarded rather than reused. + /// Neither is a state of this value. + #[must_use] + pub const fn resumed(mut self) -> Self { + if self.stopped.is_some() { + return self; + } + self.suspended = false; + self + } + + /// Whether the host has set this core aside. + #[must_use] + pub const fn is_suspended(self) -> bool { + self.suspended + } + + /// A stop was asked for and ended this way. + /// + /// A STOP IS IDEMPOTENT AND A SECOND ONE DOES NOT REPLACE THE FIRST'S + /// OUTCOME. 0115 says a second stop returns at once with the same outcome, + /// and taking the later one would let a core that reported a lane still + /// running be asked again and answer that everything stopped, which is the + /// negative disclosure turned positive by repetition. + #[must_use] + pub const fn stopped(mut self, ended: HowTheStopEnded) -> Self { + if self.stopped.is_some() { + return self; + } + self.stopped = Some(ended); + self + } + + /// How the stop ended, where one was asked for. + #[must_use] + pub const fn how_the_stop_ended(self) -> Option { + self.stopped + } + + /// Whether a stop has been asked for at all. + #[must_use] + pub const fn a_stop_was_asked_for(self) -> bool { + self.stopped.is_some() + } + + /// What a call made now does. + /// + /// IT ASKS WHETHER A STOP WAS ASKED FOR AND NEVER WHETHER IT SUCCEEDED, + /// which is 0115 in its own words: every call made after a stop was + /// requested fails, whether the stop succeeded or timed out. Reading the + /// outcome instead would leave a core whose stop expired accepting work + /// while a lane it could not stop is still running, which is the state the + /// bound exists to report rather than to continue through. + #[must_use] + pub fn what_a_call_does(self) -> WhatACallDoes { + if self.stopped.is_some() { + return WhatACallDoes::FailsWith(Failure::cancelled()); + } + WhatACallDoes::GoesAhead + } +} + +#[cfg(test)] +mod tests { + //! 0115's creation, its capability answer, its bound and its lifetime, asked + //! of the values. + //! + //! What these cannot ask is two of #115's three conditions. Stopping a set of + //! outstanding requests and proving no thread outlived the stop, and + //! suspending and resuming across a clock jump, each need something running, + //! and nothing in this tree starts a thread or makes a request. + + use super::{ + A_STOP_IS_BOUNDED_AT, HowTheStopEnded, Lane, Lifetime, StopBound, Supplied, WhatACallDoes, + }; + use crate::cache::{ByteStore, EntryKey, StorageUnavailable}; + use crate::diagnostics::{DiagnosticsSink, Event}; + use crate::failure::Kind; + use crate::session::{SecretName, SecretStore, SecretStoreUnavailable}; + use core::time::Duration; + + /// A store that answers nothing, because no case here asks it anything. + /// + /// 0115 says creation reaches no store, so a fixture that recorded its calls + /// would be proving the absence of a call this module has no way to make. + /// What it is here for is to be present. + struct AStore; + + impl ByteStore for AStore { + fn read(&self, _: &EntryKey) -> Result>, StorageUnavailable> { + Ok(None) + } + + fn write(&self, _: &EntryKey, _: &[u8]) -> Result<(), StorageUnavailable> { + Ok(()) + } + + fn remove(&self, _: &EntryKey) -> Result<(), StorageUnavailable> { + Ok(()) + } + + fn held_bytes(&self) -> Result { + Ok(0) + } + } + + struct ASecretStore; + + impl SecretStore for ASecretStore { + fn keep(&self, _: &SecretName, _: &[u8]) -> Result<(), SecretStoreUnavailable> { + Ok(()) + } + + fn read(&self, _: &SecretName) -> Result>, SecretStoreUnavailable> { + Ok(None) + } + + fn forget(&self, _: &SecretName) -> Result<(), SecretStoreUnavailable> { + Ok(()) + } + } + + struct ASink; + + impl DiagnosticsSink for ASink { + fn event(&self, _: &Event<'_>) {} + } + + /// #115's first condition names a core created with no client + /// implementations supplied at all, and 0115 makes that legal rather than + /// degraded. + #[test] + fn a_core_created_with_nothing_supplied_says_so_for_each_of_the_three() { + let present = Supplied::nothing().what_is_present(); + + assert!(present.nothing_was_supplied()); + assert!(!present.byte_store()); + assert!(!present.secret_store()); + assert!(!present.diagnostics_sink()); + } + + /// Three separate absences produce a core that works in three different + /// reduced ways, which is 0115's reason for the answer being per + /// implementation rather than one flag. + #[test] + fn each_of_the_three_is_answered_on_its_own() { + let store = AStore; + let secrets = ASecretStore; + let sink = ASink; + + let only_bytes = Supplied::nothing() + .and_the_byte_store(&store) + .what_is_present(); + assert!(only_bytes.byte_store()); + assert!(!only_bytes.secret_store()); + assert!(!only_bytes.diagnostics_sink()); + assert!(!only_bytes.nothing_was_supplied()); + + let only_secrets = Supplied::nothing() + .and_the_secret_store(&secrets) + .what_is_present(); + assert!(!only_secrets.byte_store()); + assert!(only_secrets.secret_store()); + assert!(!only_secrets.diagnostics_sink()); + + let only_events = Supplied::nothing() + .and_the_diagnostics_sink(&sink) + .what_is_present(); + assert!(!only_events.byte_store()); + assert!(!only_events.secret_store()); + assert!(only_events.diagnostics_sink()); + } + + /// All three, so that the answer is not one that happens to be right for + /// every core with one implementation in it. + #[test] + fn a_client_supplying_all_three_is_told_all_three() { + let store = AStore; + let secrets = ASecretStore; + let sink = ASink; + + let present = Supplied::nothing() + .and_the_byte_store(&store) + .and_the_secret_store(&secrets) + .and_the_diagnostics_sink(&sink) + .what_is_present(); + + assert!(present.byte_store()); + assert!(present.secret_store()); + assert!(present.diagnostics_sink()); + assert!(!present.nothing_was_supplied()); + } + + /// The default is 0115's two seconds, read out of the constant rather than + /// written twice. + #[test] + fn a_stop_a_client_bounded_at_nothing_takes_the_bound_it_was_given() { + assert_eq!(StopBound::the_default().within(), A_STOP_IS_BOUNDED_AT); + assert_eq!(A_STOP_IS_BOUNDED_AT, Duration::from_secs(2)); + + let set = StopBound::of(Duration::from_millis(500)).expect("a bound above nothing"); + assert_eq!(set.within(), Duration::from_millis(500)); + } + + /// A bound of nothing is a stop that never waits for a lane, so it reports an + /// expiry against a core that would have stopped. + #[test] + fn a_bound_of_nothing_is_refused() { + assert!(StopBound::of(Duration::ZERO).is_none()); + } + + /// A core that has just been created is running and has been asked for + /// nothing. + #[test] + fn a_created_core_is_running_and_takes_calls() { + let core = Lifetime::created(); + + assert!(!core.is_suspended()); + assert!(!core.a_stop_was_asked_for()); + assert_eq!(core.what_a_call_does(), WhatACallDoes::GoesAhead); + } + + /// 0115 says a suspend cancels nothing the caller still wants, because the + /// host has not asked for that. + #[test] + fn a_suspended_core_still_takes_a_call() { + let core = Lifetime::created().suspended(); + + assert!(core.is_suspended()); + assert_eq!(core.what_a_call_does(), WhatACallDoes::GoesAhead); + + let back = core.resumed(); + assert!(!back.is_suspended()); + assert_eq!(back.what_a_call_does(), WhatACallDoes::GoesAhead); + } + + /// Every call made after a stop was requested fails, and the kind is 0004's + /// `cancelled`, which 0115 takes knowing the fit is imperfect. + #[test] + fn a_call_after_a_stop_was_asked_for_fails_with_cancelled() { + let core = Lifetime::created().stopped(HowTheStopEnded::BothLanesStopped); + + let WhatACallDoes::FailsWith(failure) = core.what_a_call_does() else { + panic!("0115 refuses a call made after a stop was requested"); + }; + assert_eq!(failure.kind(), Kind::Cancelled); + } + + /// The half a reader gets wrong: it is whether a stop was ASKED FOR, not + /// whether it succeeded. A core whose stop expired is finished too. + #[test] + fn a_call_after_a_stop_that_expired_fails_the_same_way() { + let core = Lifetime::created().stopped(HowTheStopEnded::ALaneDidNotStop(Lane::Processing)); + + assert!(core.a_stop_was_asked_for()); + assert!(matches!( + core.what_a_call_does(), + WhatACallDoes::FailsWith(_) + )); + assert_eq!( + core.how_the_stop_ended(), + Some(HowTheStopEnded::ALaneDidNotStop(Lane::Processing)), + ); + } + + /// 0115 says a second stop returns at once with the same outcome. Taking the + /// later one would let a core that reported a lane still running be asked + /// again and answer that everything stopped. + #[test] + fn a_second_stop_does_not_replace_the_first_outcome() { + let expired = Lifetime::created().stopped(HowTheStopEnded::ALaneDidNotStop(Lane::Waiting)); + + let asked_again = expired.stopped(HowTheStopEnded::BothLanesStopped); + + assert_eq!( + asked_again.how_the_stop_ended(), + Some(HowTheStopEnded::ALaneDidNotStop(Lane::Waiting)), + ); + } + + /// There is no move back out of finished, and a host setting the process + /// aside is not one. + #[test] + fn a_finished_core_is_not_moved_by_a_suspend_or_a_resume() { + let finished = Lifetime::created().stopped(HowTheStopEnded::BothLanesStopped); + + let after = finished.suspended().resumed(); + + assert!(after.a_stop_was_asked_for()); + assert!(!after.is_suspended()); + assert!(matches!( + after.what_a_call_does(), + WhatACallDoes::FailsWith(_) + )); + } + + /// A stop that expired names which lane did not stop, because "the core + /// could not stop" tells whoever reads it nothing they can act on. + #[test] + fn the_two_lanes_are_named_apart() { + assert_ne!( + HowTheStopEnded::ALaneDidNotStop(Lane::Waiting), + HowTheStopEnded::ALaneDidNotStop(Lane::Processing), + ); + assert_eq!(Lane::Waiting.declared_name(), "waiting"); + assert_eq!(Lane::Processing.declared_name(), "processing"); + } +} diff --git a/tests/thread_statements.rs b/tests/thread_statements.rs index b2130ce..bb44dcf 100644 --- a/tests/thread_statements.rs +++ b/tests/thread_statements.rs @@ -54,6 +54,9 @@ use flowfin_core::diagnostics::{Diagnostics, DiagnosticsSink}; use flowfin_core::failure::{ Answered, Capability, Failure, FaultSite, Kind, ReadingSite, TransportOutcome, }; +use flowfin_core::lifecycle::{ + HowTheStopEnded, Lane, Lifetime, StopBound, Supplied, WhatACallDoes, WhatIsPresent, +}; use flowfin_core::measurement::{Measurement, MeasurementSink}; use flowfin_core::playback::cadence::{ ReportsWithoutWaiting, TheInterval, WhatItDoesToTheInterval, @@ -218,6 +221,17 @@ fn the_four_states_a_request_ages_through_are_safe_from_any_thread() { const _: () = any_thread::(); } +#[test] +fn the_core_lifetime_and_what_creation_takes_are_safe_from_any_thread() { + const _: () = any_thread::(); + const _: () = any_thread::(); + const _: () = any_thread::(); + const _: () = any_thread::(); + const _: () = any_thread::>(); + const _: () = any_thread::(); + const _: () = any_thread::(); +} + #[test] fn the_federation_register_is_safe_from_any_thread() { const _: () = any_thread::>();