use std::collections::VecDeque;
use std::fmt;
use std::pin::Pin;
use std::sync::{Arc, Mutex, OnceLock};
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use futures_core::Stream;
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::error::Error;
use crate::sys;
pub const DEFAULT_DEBOUNCE: Duration = Duration::from_millis(200);
pub(crate) const MIN_POLL_INTERVAL: Duration = DEFAULT_DEBOUNCE;
pub(crate) const MAX_DEBOUNCE: Duration = Duration::from_secs(24 * 60 * 60);
const MAX_QUEUED_CHANGES: usize = 1024;
#[cfg_attr(
not(any(windows, target_os = "macos", target_os = "linux")),
allow(dead_code)
)]
pub(crate) fn effective_poll_interval(requested: Duration) -> Duration {
if requested < MIN_POLL_INTERVAL {
crate::trace::warning!(
requested = ?requested,
floor = ?MIN_POLL_INTERVAL,
"poll_interval is below the minimum that avoids a busy loop; using the floor instead"
);
MIN_POLL_INTERVAL
} else {
requested
}
}
#[cfg_attr(
not(any(windows, target_os = "macos", target_os = "linux")),
allow(dead_code)
)]
pub(crate) fn effective_debounce(requested: Duration) -> Duration {
if requested > MAX_DEBOUNCE {
crate::trace::warning!(
requested = ?requested,
ceiling = ?MAX_DEBOUNCE,
"debounce is above the maximum that keeps the window's deadline representable; \
using the ceiling instead"
);
MAX_DEBOUNCE
} else {
requested
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct WatchOptions {
pub debounce: Duration,
pub watch_group_policy: bool,
pub poll_interval: Option<Duration>,
}
impl Default for WatchOptions {
fn default() -> Self {
Self::new()
}
}
impl WatchOptions {
#[must_use]
pub const fn new() -> Self {
Self {
debounce: DEFAULT_DEBOUNCE,
watch_group_policy: true,
poll_interval: None,
}
}
#[must_use]
pub const fn with_debounce(mut self, debounce: Duration) -> Self {
self.debounce = debounce;
self
}
#[must_use]
pub const fn with_group_policy(mut self, watch: bool) -> Self {
self.watch_group_policy = watch;
self
}
#[must_use]
pub const fn with_poll_interval(mut self, interval: Option<Duration>) -> Self {
self.poll_interval = interval;
self
}
}
#[cfg_attr(
not(any(
windows,
target_os = "macos",
target_os = "android",
all(
target_os = "linux",
any(feature = "linux-gnome", feature = "linux-kde")
)
)),
allow(dead_code)
)]
pub(crate) enum WatchFailSoft<T> {
Live(T),
Degraded(Error),
Fatal(Error),
}
#[cfg_attr(
not(any(
windows,
target_os = "macos",
target_os = "android",
all(
target_os = "linux",
any(feature = "linux-gnome", feature = "linux-kde")
)
)),
allow(dead_code)
)]
pub(crate) fn watch_fail_soft<T>(
leading: bool,
poll_interval: Option<Duration>,
established: Result<T, Error>,
) -> WatchFailSoft<T> {
match established {
Ok(value) => WatchFailSoft::Live(value),
Err(error) if leading && poll_interval.is_none() => WatchFailSoft::Fatal(error),
Err(error) => WatchFailSoft::Degraded(error),
}
}
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub(crate) fn watch_fail_soft_reachable<T>(
reachable: impl FnOnce() -> Result<(), Error>,
leading: bool,
poll_interval: Option<Duration>,
establish: impl FnOnce() -> Result<T, Error>,
) -> Result<WatchFailSoft<T>, Error> {
reachable()?;
Ok(watch_fail_soft(leading, poll_interval, establish()))
}
#[cfg_attr(
not(any(
windows,
target_os = "macos",
target_os = "ios",
target_os = "android",
all(
target_os = "linux",
any(feature = "linux-gnome", feature = "linux-kde")
)
)),
allow(dead_code)
)]
pub(crate) fn fatal_watch_error(what: &str, why_no_fallback: &str, source: Error) -> Error {
Error::io(
format!(
"{what} failed ({source}), and {why_no_fallback}; set WatchOptions::poll_interval \
to fall back to polling and continue anyway"
),
std::io::Error::other(source),
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BackendHealth {
pub(crate) degraded: Vec<ProxyConfigSource>,
pub(crate) has_live_notifications: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatchHealth {
pub degraded: Vec<ProxyConfigSource>,
pub has_live_notifications: bool,
pub poll_interval: Option<Duration>,
pub stopped: bool,
}
impl WatchHealth {
#[must_use]
pub fn is_fully_live(&self) -> bool {
!self.stopped && self.degraded.is_empty() && self.has_live_notifications
}
#[must_use]
pub fn is_frozen(&self) -> bool {
self.stopped || (!self.has_live_notifications && self.poll_interval.is_none())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatchState {
pub config: ProxyConfig,
pub health: WatchHealth,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum WatchEvent {
#[non_exhaustive]
Snapshot {
state: WatchState,
},
#[non_exhaustive]
Error {
error: Error,
state: WatchState,
},
}
impl WatchEvent {
#[must_use]
pub fn state(&self) -> &WatchState {
match self {
Self::Snapshot { state } | Self::Error { state, .. } => state,
}
}
}
fn merge_runtime_health(
construction: &WatchHealth,
runtime_degraded: Vec<ProxyConfigSource>,
runtime_no_live_notifications: bool,
stopped: bool,
) -> WatchHealth {
let mut degraded = construction.degraded.clone();
for source in runtime_degraded {
if !degraded.contains(&source) {
degraded.push(source);
}
}
WatchHealth {
degraded,
has_live_notifications: construction.has_live_notifications
&& !runtime_no_live_notifications,
poll_interval: construction.poll_interval,
stopped,
}
}
pub fn read() -> Result<ProxyConfig, Error> {
read_with_options(&WatchOptions::default())
}
pub fn read_with_options(options: &WatchOptions) -> Result<ProxyConfig, Error> {
read_logged(options, &sys::Env::capture())
}
#[doc(hidden)]
#[derive(Clone)]
pub struct CapturedEnv(sys::Env);
impl CapturedEnv {
#[must_use]
pub fn capture() -> Self {
Self(sys::Env::capture())
}
}
impl fmt::Debug for CapturedEnv {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CapturedEnv").finish_non_exhaustive()
}
}
#[doc(hidden)]
pub fn read_in(env: &CapturedEnv) -> Result<ProxyConfig, Error> {
read_logged(&WatchOptions::default(), &env.0)
}
fn read_logged(options: &WatchOptions, env: &sys::Env) -> Result<ProxyConfig, Error> {
crate::trace::debug!(
group_policy = options.watch_group_policy,
"reading the system proxy configuration once"
);
let config = sys::read_config_in(options, env)?;
crate::trace::debug!(
config = %crate::trace::ConfigSummary(&config),
"read the system proxy configuration"
);
Ok(config)
}
pub struct ProxyWatcher {
watch: sys::Watch,
shared: Arc<Shared>,
}
impl fmt::Debug for ProxyWatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.state();
f.debug_struct("ProxyWatcher")
.field("current", &state.config.effective)
.field("health", &state.health)
.finish_non_exhaustive()
}
}
impl ProxyWatcher {
pub fn new() -> Result<Self, Error> {
Self::with_options(WatchOptions::default())
}
pub fn with_options(options: WatchOptions) -> Result<Self, Error> {
Self::start(options, &sys::Env::capture())
}
#[doc(hidden)]
pub fn with_options_in(options: WatchOptions, env: &CapturedEnv) -> Result<Self, Error> {
Self::start(options, &env.0)
}
fn start(options: WatchOptions, env: &sys::Env) -> Result<Self, Error> {
crate::trace::debug!(
debounce = ?options.debounce,
group_policy = options.watch_group_policy,
poll_interval = ?options.poll_interval,
"starting a proxy watcher"
);
let mut watch = sys::armed_in(&options, env)?;
let initial = sys::read_config_in(&options, env)?;
crate::trace::initial(&initial);
let shared = Arc::new(Shared::new(initial));
watch.spawn(&options, Arc::clone(&shared))?;
let backend_health = watch.health();
shared.set_construction_health(WatchHealth {
degraded: backend_health.degraded,
has_live_notifications: backend_health.has_live_notifications,
poll_interval: options.poll_interval,
stopped: false,
});
Ok(Self { watch, shared })
}
#[must_use]
pub fn current(&self) -> ProxyConfig {
self.shared.current()
}
#[must_use]
pub fn current_shared(&self) -> Arc<ProxyConfig> {
self.shared.current_shared()
}
#[must_use]
pub fn health(&self) -> WatchHealth {
self.shared.health()
}
#[must_use]
pub fn state(&self) -> WatchState {
self.shared.state()
}
pub fn poll_now(&self) {
self.watch.poll_now();
}
}
impl Stream for ProxyWatcher {
type Item = WatchEvent;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.shared.poll_next(cx)
}
}
#[derive(Debug)]
pub(crate) struct Shared {
state: Mutex<State>,
construction: OnceLock<WatchHealth>,
}
#[derive(Debug)]
enum Queued {
Snapshot,
Error(Error),
}
#[derive(Debug)]
struct State {
current: Arc<ProxyConfig>,
queue: VecDeque<Queued>,
dropped: u64,
waker: Option<Waker>,
closed: bool,
dirty: bool,
runtime_degraded: Vec<ProxyConfigSource>,
runtime_no_live_notifications: bool,
}
impl State {
fn trim_to_capacity(&mut self) -> Option<u64> {
if self.queue.len() <= MAX_QUEUED_CHANGES {
return None;
}
while self.queue.len() > MAX_QUEUED_CHANGES {
self.queue.pop_front();
self.dropped += 1;
}
Some(self.dropped)
}
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn report_discarded(discarded: Option<u64>) {
match discarded {
None => {}
Some(1) => crate::trace::warning!(
capacity = MAX_QUEUED_CHANGES,
"the change queue is full, so the oldest undelivered failure was discarded; \
the subscriber is not draining the stream. `ProxyWatcher::current` stays \
accurate; what is being lost is the record of individual read failures"
),
Some(total) => crate::trace::debug!(
discarded = total,
capacity = MAX_QUEUED_CHANGES,
"the change queue is still full; discarding the oldest undelivered failure"
),
}
}
impl Shared {
pub(crate) fn new(initial: ProxyConfig) -> Self {
let mut queue = VecDeque::new();
queue.push_back(Queued::Snapshot);
Self {
state: Mutex::new(State {
current: Arc::new(initial),
queue,
dropped: 0,
waker: None,
closed: false,
dirty: false,
runtime_degraded: Vec::new(),
runtime_no_live_notifications: false,
}),
construction: OnceLock::new(),
}
}
pub(crate) fn set_construction_health(&self, health: WatchHealth) {
let already_set = self.construction.set(health).is_err();
debug_assert!(!already_set, "the constructor writes this exactly once");
}
fn construction(&self) -> &WatchHealth {
static NOT_ESTABLISHED: WatchHealth = WatchHealth {
degraded: Vec::new(),
has_live_notifications: false,
poll_interval: None,
stopped: false,
};
self.construction.get().unwrap_or(&NOT_ESTABLISHED)
}
#[cfg_attr(
not(any(windows, all(target_os = "linux", feature = "linux-kde"))),
allow(dead_code)
)]
pub(crate) fn degrade(&self, source: ProxyConfigSource) {
let mut state = self.lock();
if state.runtime_degraded.contains(&source)
|| self
.construction
.get()
.is_some_and(|health| health.degraded.contains(&source))
{
return;
}
state.runtime_degraded.push(source);
state.dirty = true;
let waker = state.waker.take();
drop(state);
if let Some(waker) = waker {
waker.wake();
}
}
pub(crate) fn mark_no_live_notifications(&self) {
let mut state = self.lock();
if state.runtime_no_live_notifications
|| self
.construction
.get()
.is_some_and(|health| !health.has_live_notifications)
{
return;
}
state.runtime_no_live_notifications = true;
state.dirty = true;
let waker = state.waker.take();
drop(state);
if let Some(waker) = waker {
waker.wake();
}
}
pub(crate) fn health(&self) -> WatchHealth {
let state = self.lock();
self.merged_health(&state)
}
pub(crate) fn state(&self) -> WatchState {
let state = self.lock();
self.stamp(&state)
}
fn merged_health(&self, state: &State) -> WatchHealth {
merge_runtime_health(
self.construction(),
state.runtime_degraded.clone(),
state.runtime_no_live_notifications,
state.closed,
)
}
fn stamp(&self, state: &State) -> WatchState {
WatchState {
config: ProxyConfig::clone(&state.current),
health: self.merged_health(state),
}
}
pub(crate) fn current(&self) -> ProxyConfig {
ProxyConfig::clone(&self.current_shared())
}
pub(crate) fn current_shared(&self) -> Arc<ProxyConfig> {
Arc::clone(&self.lock().current)
}
#[cfg_attr(
all(
not(any(
windows,
target_os = "macos",
target_os = "linux",
target_os = "android",
target_os = "ios"
)),
not(test)
),
expect(dead_code)
)]
pub(crate) fn emit(&self, config: ProxyConfig) {
let mut state = self.lock();
if *state.current == config {
drop(state);
crate::trace::debug!(
config = %crate::trace::ConfigSummary(&config),
"an unchanged snapshot was skipped (equality skip)"
);
return;
}
let config = Arc::new(config);
let previous = std::mem::replace(&mut state.current, Arc::clone(&config));
let last = state
.queue
.iter()
.rposition(|i| matches!(i, Queued::Snapshot));
let folded = match last {
Some(at) if at + 1 == state.queue.len() => true,
Some(at) if at > 0 => {
state.queue.remove(at);
state.queue.push_back(Queued::Snapshot);
true
}
_ => {
state.queue.push_back(Queued::Snapshot);
false
}
};
state.dirty = false;
let discarded = state.trim_to_capacity();
let waker = state.waker.take();
drop(state);
crate::trace::changed(&previous, &config);
if folded {
crate::trace::debug!(
"a newer snapshot replaced one the subscriber had not taken yet; \
what it will read is the current configuration, not the intermediate one"
);
}
report_discarded(discarded);
if let Some(waker) = waker {
waker.wake();
}
}
pub(crate) fn fail(&self, error: Error) {
crate::trace::warning!(
error = %crate::trace::SafeError(&error),
"reading the proxy configuration failed; the subscription stays open"
);
let mut state = self.lock();
if !state.queue.is_empty() {
debug_assert!(
state.waker.is_none(),
"a waker must not be registered while the queue is non-empty"
);
}
if let Some(Queued::Error(last)) = state.queue.back_mut() {
*last = error;
return;
}
state.queue.push_back(Queued::Error(error));
let waker = state.waker.take();
drop(state);
if let Some(waker) = waker {
waker.wake();
}
}
pub(crate) fn close(&self) {
let mut state = self.lock();
if state.closed {
return;
}
state.closed = true;
state.dirty = true;
let waker = state.waker.take();
drop(state);
crate::trace::debug!("the watcher thread finished; the stream will end");
if let Some(waker) = waker {
waker.wake();
}
}
fn poll_next(&self, cx: &mut Context<'_>) -> Poll<Option<WatchEvent>> {
let waker = cx.waker().clone();
let mut state = self.lock();
if let Some(item) = state.queue.pop_front() {
if state.queue.is_empty() {
state.dropped = 0;
}
let event = match item {
Queued::Snapshot => {
state.dirty = false;
WatchEvent::Snapshot {
state: self.stamp(&state),
}
}
Queued::Error(error) => WatchEvent::Error {
error,
state: self.stamp(&state),
},
};
return Poll::Ready(Some(event));
}
if state.dirty {
state.dirty = false;
return Poll::Ready(Some(WatchEvent::Snapshot {
state: self.stamp(&state),
}));
}
if state.closed {
return Poll::Ready(None);
}
let previous = state.waker.replace(waker);
drop(state);
drop(previous);
Poll::Pending
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
pub(crate) struct ThreadGuard {
shared: Arc<Shared>,
}
#[cfg_attr(
not(any(windows, target_os = "macos", target_os = "linux")),
allow(dead_code)
)]
impl ThreadGuard {
pub(crate) fn new(shared: Arc<Shared>) -> Self {
Self { shared }
}
}
impl Drop for ThreadGuard {
fn drop(&mut self) {
if std::thread::panicking() {
crate::trace::error!("the proxy-watch backend thread panicked; closing the stream");
self.shared.mark_no_live_notifications();
self.shared.fail(Error::io(
"running the proxy-watch backend thread",
std::io::Error::other("the watcher thread panicked"),
));
}
self.shared.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::Wake;
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<ProxyWatcher>();
assert_send::<WatchOptions>();
};
const _: fn() = || {
trait AmbiguousIfSync<A> {
fn item() {}
}
impl<T: ?Sized> AmbiguousIfSync<()> for T {}
impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
let _ = <ProxyWatcher as AmbiguousIfSync<_>>::item;
};
const _: fn(&ProxyWatcher) = |watcher| watcher.poll_now();
const _: fn(&ProxyWatcher) -> WatchState = ProxyWatcher::state;
#[test]
fn the_initial_value_is_queued_once_and_equality_skips_repeats() {
let initial = ProxyConfig::direct();
let shared = Shared::new(initial.clone());
assert_eq!(shared.lock().queue.len(), 1);
shared.emit(ProxyConfig::direct());
assert_eq!(shared.lock().queue.len(), 1);
assert!(!shared.lock().dirty, "an equal read changes nothing");
shared.emit(ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
assert_eq!(shared.lock().queue.len(), 1);
assert_eq!(
shared.current().effective,
crate::mode::ProxyMode::WpadAutoDetect
);
}
#[test]
fn a_shared_snapshot_is_not_rewritten_by_a_later_change() {
let shared = Shared::new(ProxyConfig::direct());
let before = shared.current_shared();
shared.emit(ProxyConfig::direct());
assert!(Arc::ptr_eq(&before, &shared.current_shared()));
shared.emit(ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
assert_eq!(before.effective, crate::mode::ProxyMode::Direct);
assert_eq!(
shared.current_shared().effective,
crate::mode::ProxyMode::WpadAutoDetect
);
assert_eq!(*shared.current_shared(), shared.current());
}
#[test]
fn a_snapshot_that_differs_only_in_fallbacks_is_published_rather_than_skipped() {
let complete = ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::Direct,
);
let degraded = complete
.clone()
.with_fallbacks(vec![crate::config::ProxyConfigSource::GroupPolicy]);
let shared = Shared::new(complete.clone());
shared.emit(degraded);
assert_eq!(
shared.current().fallbacks,
[crate::config::ProxyConfigSource::GroupPolicy],
"a read that lost a source is not the snapshot that never had one"
);
shared.emit(complete);
assert!(
shared.current().fallbacks.is_empty(),
"and the recovery is a change too"
);
}
#[test]
fn a_failure_before_the_first_poll_stays_behind_the_subscription_snapshot() {
let shared = Shared::new(ProxyConfig::direct());
shared.fail(Error::io(
"reading the proxy configuration",
std::io::Error::other("a read that failed before the first poll"),
));
shared.emit(ProxyConfig::from_source(
ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
let mut cx = Context::from_waker(Waker::noop());
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => {
assert_eq!(
state.config.effective,
crate::mode::ProxyMode::WpadAutoDetect
);
}
other => panic!("the first item must be the subscription snapshot, got {other:?}"),
}
assert!(matches!(
shared.poll_next(&mut cx),
Poll::Ready(Some(WatchEvent::Error { .. }))
));
assert!(matches!(
shared.poll_next(&mut cx),
Poll::Ready(Some(WatchEvent::Snapshot { .. }))
));
assert!(shared.poll_next(&mut cx).is_pending());
}
#[test]
fn a_change_after_a_queued_failure_gets_a_snapshot_behind_it() {
let shared = Shared::new(ProxyConfig::direct());
let mut cx = Context::from_waker(Waker::noop());
assert!(matches!(
shared.poll_next(&mut cx),
Poll::Ready(Some(WatchEvent::Snapshot { .. }))
));
let changed = |mode| ProxyConfig::from_source(ProxyConfigSource::Registry, mode);
shared.emit(changed(crate::mode::ProxyMode::WpadAutoDetect));
shared.fail(sandboxed_error("between"));
shared.emit(changed(crate::mode::ProxyMode::Direct));
shared.emit(changed(crate::mode::ProxyMode::WpadAutoDetect));
let kinds: Vec<&str> = std::iter::from_fn(|| match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { .. })) => Some("snapshot"),
Poll::Ready(Some(WatchEvent::Error { .. })) => Some("error"),
_ => None,
})
.collect();
assert_eq!(kinds, ["snapshot", "error", "snapshot"]);
}
fn live_construction_health() -> WatchHealth {
WatchHealth {
degraded: Vec::new(),
has_live_notifications: true,
poll_interval: None,
stopped: false,
}
}
#[test]
fn one_shared_state_read_contains_config_and_runtime_health() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
shared.emit(ProxyConfig::from_source(
ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
shared.degrade(ProxyConfigSource::Registry);
shared.mark_no_live_notifications();
shared.close();
let state = shared.state();
assert_eq!(
state.config.effective,
crate::mode::ProxyMode::WpadAutoDetect
);
assert_eq!(state.health.degraded, vec![ProxyConfigSource::Registry]);
assert!(!state.health.has_live_notifications);
assert!(state.health.stopped);
}
#[test]
fn an_unwritten_construction_health_claims_no_route() {
let shared = Shared::new(ProxyConfig::direct());
let health = shared.health();
assert!(!health.has_live_notifications);
assert!(health.degraded.is_empty());
assert_eq!(health.poll_interval, None);
assert!(!health.stopped);
assert!(!health.is_fully_live());
}
fn two_distinct_configs() -> (ProxyConfig, ProxyConfig) {
(
ProxyConfig::direct(),
ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
),
)
}
fn fill_past_capacity(shared: &Shared, rounds: usize) {
let (direct, wpad) = two_distinct_configs();
let mut cx = Context::from_waker(Waker::noop());
assert!(shared.poll_next(&mut cx).is_ready());
shared.fail(sandboxed_error("filler"));
for index in 0..rounds {
shared.emit(if index % 2 == 0 {
wpad.clone()
} else {
direct.clone()
});
shared.fail(sandboxed_error("filler"));
}
}
#[test]
fn a_stalled_consumer_cannot_grow_the_queue_without_bound() {
let shared = Shared::new(ProxyConfig::direct());
fill_past_capacity(&shared, MAX_QUEUED_CHANGES * 2);
let state = shared.lock();
assert!(
state.queue.len() <= MAX_QUEUED_CHANGES + 1,
"queue grew to {}",
state.queue.len()
);
assert!(state.dropped > 0, "the cap must have discarded something");
}
#[test]
fn undelivered_snapshots_fold_into_the_newest_one() {
let (direct, wpad) = two_distinct_configs();
let shared = Shared::new(direct.clone());
let mut cx = Context::from_waker(Waker::noop());
for index in 0..64 {
shared.emit(if index % 2 == 0 {
wpad.clone()
} else {
direct.clone()
});
}
assert_eq!(shared.lock().queue.len(), 1);
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => {
assert_eq!(state.config, shared.current());
assert_eq!(state.config, direct);
}
other => panic!("expected the newest snapshot, got {other:?}"),
}
assert!(shared.poll_next(&mut cx).is_pending());
}
#[test]
fn draining_the_queue_resets_the_discard_counter() {
let shared = Shared::new(ProxyConfig::direct());
let mut cx = Context::from_waker(Waker::noop());
fill_past_capacity(&shared, MAX_QUEUED_CHANGES + 5);
assert!(shared.lock().dropped > 0);
while shared.poll_next(&mut cx).is_ready() {
if shared.lock().queue.is_empty() {
break;
}
}
assert_eq!(shared.lock().dropped, 0);
assert_eq!(shared.lock().queue.len(), 0);
}
#[test]
fn a_degrade_reaches_the_stream_even_when_every_later_read_is_equal() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let mut cx = Context::from_waker(Waker::noop());
assert!(matches!(
shared.poll_next(&mut cx),
Poll::Ready(Some(WatchEvent::Snapshot { .. }))
));
assert!(shared.poll_next(&mut cx).is_pending());
shared.degrade(ProxyConfigSource::Registry);
shared.emit(ProxyConfig::direct());
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => {
assert_eq!(state.health.degraded, vec![ProxyConfigSource::Registry]);
assert_eq!(state.config, ProxyConfig::direct());
}
other => panic!("the lost route must reach the stream, got {other:?}"),
}
assert!(shared.poll_next(&mut cx).is_pending());
shared.degrade(ProxyConfigSource::Registry);
assert!(shared.poll_next(&mut cx).is_pending());
}
#[test]
fn losing_the_last_route_wakes_a_parked_consumer() {
struct Signal(AtomicBool);
impl Wake for Signal {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.store(true, Ordering::SeqCst);
}
}
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let signal = Arc::new(Signal(AtomicBool::new(false)));
let waker = Waker::from(Arc::clone(&signal));
let mut cx = Context::from_waker(&waker);
assert!(shared.poll_next(&mut cx).is_ready());
assert!(shared.poll_next(&mut cx).is_pending());
assert!(!signal.0.load(Ordering::SeqCst));
shared.mark_no_live_notifications();
assert!(
signal.0.load(Ordering::SeqCst),
"losing the last route must wake the parked consumer"
);
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => {
assert!(!state.health.has_live_notifications);
}
other => panic!("expected a snapshot reporting the loss, got {other:?}"),
}
assert!(shared.poll_next(&mut cx).is_pending());
signal.0.store(false, Ordering::SeqCst);
shared.mark_no_live_notifications();
assert!(!signal.0.load(Ordering::SeqCst));
assert!(shared.poll_next(&mut cx).is_pending());
}
#[test]
fn a_second_poll_takes_the_waker_slot_from_the_first_consumer() {
struct Signal(AtomicBool);
impl Wake for Signal {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.store(true, Ordering::SeqCst);
}
}
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let first = Arc::new(Signal(AtomicBool::new(false)));
let second = Arc::new(Signal(AtomicBool::new(false)));
let first_waker = Waker::from(Arc::clone(&first));
let second_waker = Waker::from(Arc::clone(&second));
assert!(
shared
.poll_next(&mut Context::from_waker(&first_waker))
.is_ready()
);
assert!(
shared
.poll_next(&mut Context::from_waker(&first_waker))
.is_pending()
);
assert!(
shared
.poll_next(&mut Context::from_waker(&second_waker))
.is_pending()
);
shared.mark_no_live_notifications();
assert!(
second.0.load(Ordering::SeqCst),
"the most recently registered waker is the one that is kept"
);
assert!(
!first.0.load(Ordering::SeqCst),
"the first consumer stays parked: nothing on this watcher is left to wake it"
);
}
#[test]
fn closing_delivers_a_stopped_snapshot_exactly_once_before_the_end() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let mut cx = Context::from_waker(Waker::noop());
assert!(shared.poll_next(&mut cx).is_ready());
shared.close();
shared.close();
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => {
assert!(state.health.stopped);
assert!(state.health.is_frozen());
}
other => panic!("expected a stopped snapshot before the end, got {other:?}"),
}
assert!(matches!(shared.poll_next(&mut cx), Poll::Ready(None)));
assert!(matches!(shared.poll_next(&mut cx), Poll::Ready(None)));
}
#[test]
fn a_failure_does_not_stand_in_for_the_terminal_snapshot() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let mut cx = Context::from_waker(Waker::noop());
assert!(shared.poll_next(&mut cx).is_ready());
shared.fail(sandboxed_error("dying"));
shared.close();
assert!(matches!(
shared.poll_next(&mut cx),
Poll::Ready(Some(WatchEvent::Error { .. }))
));
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Snapshot { state })) => assert!(state.health.stopped),
other => panic!("expected the terminal snapshot after the failure, got {other:?}"),
}
assert!(matches!(shared.poll_next(&mut cx), Poll::Ready(None)));
}
#[test]
fn every_event_carries_the_state_it_was_delivered_with() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let mut cx = Context::from_waker(Waker::noop());
let Poll::Ready(Some(snapshot)) = shared.poll_next(&mut cx) else {
panic!("the subscription-time snapshot is always ready");
};
assert_eq!(snapshot.state(), &shared.state());
shared.fail(sandboxed_error("only a read failed"));
let Poll::Ready(Some(failure)) = shared.poll_next(&mut cx) else {
panic!("the queued failure is always ready");
};
assert!(matches!(failure, WatchEvent::Error { .. }));
assert_eq!(failure.state(), &shared.state());
assert!(failure.state().health.has_live_notifications);
}
fn sandboxed_error(reason: &str) -> Error {
Error::Sandboxed {
sandbox: "Flatpak".to_owned(),
reason: reason.to_owned(),
}
}
#[test]
fn consecutive_fails_coalesce_into_the_newest_error() {
let shared = Shared::new(ProxyConfig::direct());
assert!(matches!(
shared.lock().queue.pop_front(),
Some(Queued::Snapshot)
));
shared.fail(sandboxed_error("first"));
assert_eq!(shared.lock().queue.len(), 1);
shared.fail(sandboxed_error("second"));
assert_eq!(shared.lock().queue.len(), 1);
shared.fail(sandboxed_error("third"));
assert_eq!(shared.lock().queue.len(), 1);
let state = shared.lock();
match &state.queue[0] {
Queued::Error(Error::Sandboxed { reason, .. }) => assert_eq!(reason, "third"),
other => panic!("expected the newest coalesced Sandboxed error, got {other:?}"),
}
}
#[test]
fn fail_after_an_intervening_emit_is_not_coalesced() {
let shared = Shared::new(ProxyConfig::direct());
assert!(matches!(
shared.lock().queue.pop_front(),
Some(Queued::Snapshot)
));
shared.fail(sandboxed_error("before"));
shared.emit(ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
shared.fail(sandboxed_error("after"));
let state = shared.lock();
assert_eq!(state.queue.len(), 3);
match &state.queue[0] {
Queued::Error(Error::Sandboxed { reason, .. }) => assert_eq!(reason, "before"),
other => panic!("expected the first Sandboxed error, got {other:?}"),
}
assert!(matches!(state.queue[1], Queued::Snapshot));
match &state.queue[2] {
Queued::Error(Error::Sandboxed { reason, .. }) => assert_eq!(reason, "after"),
other => panic!("expected the second Sandboxed error, got {other:?}"),
}
}
#[test]
fn a_parked_consumer_is_woken_once_and_sees_one_coalesced_error() {
struct Signal(AtomicBool);
impl Wake for Signal {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.store(true, Ordering::SeqCst);
}
}
let shared = Shared::new(ProxyConfig::direct());
assert!(matches!(
shared.lock().queue.pop_front(),
Some(Queued::Snapshot)
));
let signal = Arc::new(Signal(AtomicBool::new(false)));
let waker = Waker::from(Arc::clone(&signal));
let mut cx = Context::from_waker(&waker);
assert!(shared.poll_next(&mut cx).is_pending());
assert!(!signal.0.load(Ordering::SeqCst));
shared.fail(sandboxed_error("first"));
assert!(
signal.0.load(Ordering::SeqCst),
"the first failure must wake the parked consumer"
);
shared.fail(sandboxed_error("second"));
match shared.poll_next(&mut cx) {
Poll::Ready(Some(WatchEvent::Error {
error: Error::Sandboxed { reason, .. },
..
})) => {
assert_eq!(reason, "second");
}
other => panic!("expected exactly one coalesced Sandboxed error, got {other:?}"),
}
assert!(shared.lock().queue.is_empty());
}
#[test]
fn the_documented_defaults_and_bounds_are_the_stated_numbers() {
let options = WatchOptions::new();
assert_eq!(options.debounce, DEFAULT_DEBOUNCE);
assert!(options.watch_group_policy);
assert_eq!(options.poll_interval, None);
assert_eq!(DEFAULT_DEBOUNCE, Duration::from_millis(200));
assert_eq!(MIN_POLL_INTERVAL, Duration::from_millis(200));
assert_eq!(MAX_DEBOUNCE, Duration::from_secs(24 * 60 * 60));
}
#[test]
fn effective_poll_interval_clamps_to_the_floor() {
let cases = [
(Duration::ZERO, MIN_POLL_INTERVAL),
(Duration::from_millis(1), MIN_POLL_INTERVAL),
(
MIN_POLL_INTERVAL - Duration::from_millis(1),
MIN_POLL_INTERVAL,
),
(MIN_POLL_INTERVAL, MIN_POLL_INTERVAL),
(
MIN_POLL_INTERVAL + Duration::from_secs(1),
MIN_POLL_INTERVAL + Duration::from_secs(1),
),
(Duration::from_secs(30), Duration::from_secs(30)),
];
for (requested, expected) in cases {
assert_eq!(
effective_poll_interval(requested),
expected,
"requested {requested:?}"
);
}
}
#[test]
fn effective_debounce_clamps_to_the_ceiling() {
let cases = [
(Duration::ZERO, Duration::ZERO),
(DEFAULT_DEBOUNCE, DEFAULT_DEBOUNCE),
(
MAX_DEBOUNCE - Duration::from_millis(1),
MAX_DEBOUNCE - Duration::from_millis(1),
),
(MAX_DEBOUNCE, MAX_DEBOUNCE),
(MAX_DEBOUNCE + Duration::from_millis(1), MAX_DEBOUNCE),
(Duration::MAX, MAX_DEBOUNCE),
];
for (requested, expected) in cases {
assert_eq!(
effective_debounce(requested),
expected,
"requested {requested:?}"
);
}
}
#[test]
fn a_clamped_debounce_keeps_the_deadline_representable() {
for requested in [Duration::MAX, MAX_DEBOUNCE + Duration::from_secs(1)] {
let options = WatchOptions::new().with_debounce(requested);
assert!(
std::time::Instant::now()
.checked_add(effective_debounce(options.debounce))
.is_some(),
"requested {requested:?}"
);
}
}
#[test]
fn watch_health_live_and_frozen_flags() {
let cases = [
(Vec::new(), true, None, false, true, false),
(
vec![ProxyConfigSource::GroupPolicy],
true,
None,
false,
false,
false,
),
(Vec::new(), false, None, false, false, true),
(
Vec::new(),
false,
Some(Duration::from_secs(30)),
false,
false,
false,
),
(
Vec::new(),
false,
Some(Duration::from_secs(30)),
true,
false,
true,
),
(Vec::new(), true, None, true, false, true),
];
for (degraded, has_live, poll_interval, stopped, fully_live, frozen) in cases {
let health = WatchHealth {
degraded,
has_live_notifications: has_live,
poll_interval,
stopped,
};
assert_eq!(health.is_fully_live(), fully_live, "{health:?}");
assert_eq!(health.is_frozen(), frozen, "{health:?}");
}
}
fn join_silently(body: impl FnOnce() + Send + 'static) -> std::thread::Result<()> {
static HOOK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _serial = HOOK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::thread::spawn(body).join();
std::panic::set_hook(previous);
result
}
#[test]
fn a_panicking_watcher_thread_still_closes_the_stream() {
let shared = Arc::new(Shared::new(ProxyConfig::direct()));
let result = {
let shared = Arc::clone(&shared);
join_silently(move || {
let _guard = ThreadGuard::new(Arc::clone(&shared));
panic!("simulated backend panic");
})
};
assert!(result.is_err());
let state = shared.lock();
assert_eq!(state.queue.len(), 2);
assert!(matches!(state.queue[0], Queued::Snapshot));
assert!(matches!(state.queue[1], Queued::Error(_)));
assert!(state.closed);
assert!(state.runtime_no_live_notifications);
assert!(state.runtime_degraded.is_empty());
}
#[test]
fn a_thread_that_panicked_holding_the_lock_still_gets_its_failure_delivered() {
let shared = Arc::new(Shared::new(ProxyConfig::direct()));
let result = {
let shared = Arc::clone(&shared);
join_silently(move || {
let _guard = ThreadGuard::new(Arc::clone(&shared));
let _held = shared.lock();
panic!("simulated backend panic inside the critical section");
})
};
assert!(result.is_err());
assert!(shared.state.is_poisoned());
let state = shared.lock();
assert_eq!(state.queue.len(), 2);
assert!(matches!(state.queue[0], Queued::Snapshot));
assert!(matches!(state.queue[1], Queued::Error(_)));
assert!(state.closed);
assert!(state.runtime_no_live_notifications);
}
#[cfg(feature = "tracing")]
#[test]
fn no_subscriber_ever_runs_while_the_state_mutex_is_held() {
use tracing::subscriber::Interest;
struct Probe {
shared: Arc<Shared>,
held: Mutex<Vec<(&'static str, bool)>>,
}
impl Probe {
fn note(&self, hook: &'static str) {
let held = self.shared.state.try_lock().is_err();
self.held.lock().unwrap().push((hook, held));
}
}
struct Wired(Arc<Probe>);
impl tracing::Subscriber for Wired {
fn register_callsite(&self, _: &tracing::Metadata<'_>) -> Interest {
Interest::sometimes()
}
fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
self.0.note("enabled");
true
}
fn event(&self, _: &tracing::Event<'_>) {
self.0.note("event");
}
fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn record(&self, _: &tracing::Id, _: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _: &tracing::Id, _: &tracing::Id) {}
fn enter(&self, _: &tracing::Id) {}
fn exit(&self, _: &tracing::Id) {}
}
let shared = Arc::new(Shared::new(ProxyConfig::direct()));
let probe = Arc::new(Probe {
shared: Arc::clone(&shared),
held: Mutex::new(Vec::new()),
});
tracing::subscriber::with_default(Wired(Arc::clone(&probe)), || {
shared.emit(ProxyConfig::direct());
shared.emit(ProxyConfig::from_source(
crate::config::ProxyConfigSource::Registry,
crate::mode::ProxyMode::WpadAutoDetect,
));
shared.close();
});
let observed = probe.held.lock().unwrap();
assert!(
observed.iter().any(|(hook, _)| *hook == "enabled"),
"the level filter has to have been consulted"
);
assert!(
observed.iter().any(|(hook, _)| *hook == "event"),
"an event has to have been emitted"
);
assert!(observed.iter().all(|(_, held)| !held), "{observed:?}");
}
#[test]
fn a_panicking_guard_marks_the_route_lost_before_it_wakes_anyone() {
struct Probe {
shared: Arc<Shared>,
live_at_wake: Mutex<Vec<bool>>,
}
impl Wake for Probe {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
let live = self.shared.health().has_live_notifications;
self.live_at_wake.lock().unwrap().push(live);
}
}
let shared = Arc::new(Shared::new(ProxyConfig::direct()));
shared.set_construction_health(live_construction_health());
let probe = Arc::new(Probe {
shared: Arc::clone(&shared),
live_at_wake: Mutex::new(Vec::new()),
});
let waker = Waker::from(Arc::clone(&probe));
let mut cx = Context::from_waker(&waker);
assert!(shared.poll_next(&mut cx).is_ready());
assert!(shared.poll_next(&mut cx).is_pending());
let result = {
let shared = Arc::clone(&shared);
join_silently(move || {
let _guard = ThreadGuard::new(shared);
panic!("simulated backend panic");
})
};
assert!(result.is_err());
assert_eq!(
*probe.live_at_wake.lock().unwrap(),
vec![false],
"the consumer must be woken once, and told the route is gone"
);
}
#[test]
fn parking_never_runs_executor_code_under_the_state_mutex() {
struct Probe {
shared: Arc<Shared>,
held_at_drop: Arc<Mutex<Vec<bool>>>,
}
impl Wake for Probe {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {}
}
impl Drop for Probe {
fn drop(&mut self) {
let held = self.shared.state.try_lock().is_err();
self.held_at_drop.lock().unwrap().push(held);
}
}
let shared = Arc::new(Shared::new(ProxyConfig::direct()));
let held_at_drop = Arc::new(Mutex::new(Vec::new()));
let probe = Arc::new(Probe {
shared: Arc::clone(&shared),
held_at_drop: Arc::clone(&held_at_drop),
});
let waker = Waker::from(Arc::clone(&probe));
assert!(
shared
.poll_next(&mut Context::from_waker(&waker))
.is_ready()
);
assert!(
shared
.poll_next(&mut Context::from_waker(&waker))
.is_pending()
);
drop(waker);
drop(probe);
assert!(
shared
.poll_next(&mut Context::from_waker(Waker::noop()))
.is_pending()
);
let observed = held_at_drop.lock().unwrap();
assert_eq!(
observed.len(),
1,
"the parked waker has to have been dropped"
);
assert!(observed.iter().all(|held| !held), "{observed:?}");
}
fn some_error() -> Error {
Error::Unsupported
}
#[test]
fn an_unreachable_platform_fails_the_route_as_it_is_whatever_the_interval() {
let unreachable = || Err(Error::io("attaching", std::io::Error::other("no JavaVM")));
for poll_interval in [None, Some(Duration::from_secs(1))] {
let mut established = false;
let outcome = watch_fail_soft_reachable(unreachable, true, poll_interval, || {
established = true;
Ok(())
});
let Err(error) = outcome else {
panic!("an unreachable platform was softened under {poll_interval:?}");
};
assert!(
matches!(&error, Error::Io { context, .. } if context == "attaching"),
"{error:?}"
);
assert!(
!established,
"the route was attempted with no platform to reach"
);
}
let failed = || Err::<(), _>(Error::Unsupported);
assert!(matches!(
watch_fail_soft_reachable(|| Ok(()), true, None, failed),
Ok(WatchFailSoft::Fatal(Error::Unsupported))
));
assert!(matches!(
watch_fail_soft_reachable(|| Ok(()), true, Some(Duration::from_secs(1)), failed),
Ok(WatchFailSoft::Degraded(Error::Unsupported))
));
}
#[test]
fn watch_fail_soft_outcomes() {
enum Input {
Ok,
Err,
}
enum Expect {
Live,
Degraded,
Fatal,
}
let cases: &[(bool, Option<Duration>, Input, Expect)] = &[
(true, None, Input::Ok, Expect::Live),
(false, None, Input::Ok, Expect::Live),
(true, Some(Duration::from_secs(1)), Input::Ok, Expect::Live),
(false, Some(Duration::from_secs(1)), Input::Ok, Expect::Live),
(false, None, Input::Err, Expect::Degraded),
(
false,
Some(Duration::from_secs(1)),
Input::Err,
Expect::Degraded,
),
(
true,
Some(Duration::from_secs(30)),
Input::Err,
Expect::Degraded,
),
(true, None, Input::Err, Expect::Fatal),
];
for (leading, poll_interval, input, expect) in cases {
let outcome = watch_fail_soft(
*leading,
*poll_interval,
match input {
Input::Ok => Ok(42),
Input::Err => Err(some_error()),
},
);
match expect {
Expect::Live => assert!(
matches!(outcome, WatchFailSoft::Live(42)),
"leading = {leading}, poll_interval = {poll_interval:?}"
),
Expect::Degraded => assert!(
matches!(outcome, WatchFailSoft::Degraded(_)),
"leading = {leading}, poll_interval = {poll_interval:?}"
),
Expect::Fatal => assert!(
matches!(outcome, WatchFailSoft::Fatal(_)),
"leading = {leading}, poll_interval = {poll_interval:?}"
),
}
}
}
#[test]
fn fatal_watch_error_names_the_fix_and_keeps_the_context() {
let error = fatal_watch_error(
"opening or arming the HKCU Internet Settings key",
"HKCU is Windows' only mandatory proxy source",
some_error(),
);
let message = error.to_string();
assert!(
message.contains("poll_interval"),
"the error should point at the fix: {message}"
);
assert!(
message.contains("opening or arming the HKCU Internet Settings key"),
"the error should still say what failed: {message}"
);
assert!(
message.contains("HKCU is Windows' only mandatory proxy source"),
"the error should still say why there is no fallback: {message}"
);
}
#[test]
fn shared_degrade_is_idempotent() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
let health = shared.health();
assert!(health.degraded.is_empty());
assert!(health.has_live_notifications);
assert!(!health.stopped);
shared.degrade(ProxyConfigSource::GroupPolicy);
shared.degrade(ProxyConfigSource::GroupPolicy);
let health = shared.health();
assert_eq!(health.degraded, vec![ProxyConfigSource::GroupPolicy]);
assert!(health.has_live_notifications);
assert!(!health.stopped);
}
#[test]
fn shared_mark_no_live_notifications_is_reflected_in_health() {
let shared = Shared::new(ProxyConfig::direct());
shared.set_construction_health(live_construction_health());
assert!(shared.health().has_live_notifications);
shared.mark_no_live_notifications();
assert!(!shared.health().has_live_notifications);
}
#[test]
fn merge_runtime_health_folds_in_new_state_without_disturbing_the_old() {
let construction = WatchHealth {
degraded: vec![ProxyConfigSource::GroupPolicy],
has_live_notifications: true,
poll_interval: None,
stopped: false,
};
let health = merge_runtime_health(&construction, Vec::new(), false, false);
assert_eq!(health.degraded, vec![ProxyConfigSource::GroupPolicy]);
assert!(health.has_live_notifications);
let health = merge_runtime_health(
&construction,
vec![ProxyConfigSource::Registry, ProxyConfigSource::GroupPolicy],
false,
false,
);
assert_eq!(
health.degraded,
vec![ProxyConfigSource::GroupPolicy, ProxyConfigSource::Registry]
);
assert!(health.has_live_notifications);
let health = merge_runtime_health(&construction, Vec::new(), true, false);
assert!(!health.has_live_notifications);
}
#[test]
fn a_poll_interval_stops_counting_once_the_thread_is_gone() {
let construction = WatchHealth {
degraded: Vec::new(),
has_live_notifications: true,
poll_interval: Some(Duration::from_secs(30)),
stopped: false,
};
let live = merge_runtime_health(&construction, Vec::new(), false, false);
assert!(!live.is_frozen());
let panicked = merge_runtime_health(&construction, Vec::new(), true, true);
assert!(panicked.is_frozen());
assert_eq!(
panicked.poll_interval,
Some(Duration::from_secs(30)),
"the interval is still what the caller asked for"
);
}
}