Skip to main content

governor/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Reusable charge governor runtime framework for Fuchsia.
6//!
7//! Provides the [`Governor`] trait and [`run_governor`], which bridges the battery and
8//! charger telemetry served by battery-manager to voted [`fcharger::ControllerMarker`] settings.
9
10use anyhow::{Context as _, Result, anyhow};
11use fidl_fuchsia_hardware_power_battery as fbattery;
12use fidl_fuchsia_hardware_power_charger as fcharger;
13use fuchsia_async as fasync;
14use fuchsia_component::client::connect_to_protocol;
15use futures::FutureExt;
16use log::{debug, info, warn};
17use std::convert::Infallible;
18
19const RECONNECT_DELAY: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2);
20
21/// Combined hardware telemetry snapshot passed to [`Governor::evaluate`].
22#[derive(Clone, Debug, Default, PartialEq)]
23pub struct Telemetry {
24    pub battery: fbattery::Status,
25    pub charger: Option<fcharger::Status>,
26}
27
28/// Desired vote and optional periodic sampling, returned by [`Governor::evaluate`].
29#[derive(Clone, Debug, Default, PartialEq)]
30pub struct Evaluation {
31    /// Complete desired vote for this governor session. A `None` field withdraws this governor's
32    /// vote on it, so that field relaxes to the other governors' votes or the charger default.
33    pub vote: fcharger::ControlOptions,
34    /// If `Some(interval)`, battery-manager is asked to deliver a freshly sampled battery status at
35    /// least this often, so the governor is re-evaluated even when no watched field changes.
36    /// battery-manager may clamp the interval. If `None`, the governor is re-evaluated only when
37    /// the telemetry changes.
38    pub sample_interval: Option<zx::BootDuration>,
39    /// Whether a sample that falls due while the system is suspended wakes the system, rather than
40    /// waiting for it to resume. Waking the system costs power, so a governor sets this only while
41    /// its samples must not wait, e.g. while charging. Ignored without `sample_interval`.
42    pub wake_for_samples: bool,
43}
44
45/// Battery and charger fields whose changes trigger an evaluation, and those whose changes also
46/// wake the system from suspend to do so. Each is a `Status` mask: only field presence matters.
47#[derive(Clone, Debug, Default, PartialEq)]
48pub struct WatchInterest {
49    /// Sent as the battery `WatchOptions.interest`. `None` keeps battery-manager's default, which
50    /// is every field it can watch. Changes to other fields are seen at the next evaluation, e.g.
51    /// the next sample requested through [`Evaluation::sample_interval`].
52    pub battery: Option<fbattery::Status>,
53    /// Sent as the charger `WatchOptions.interest`, like `battery`.
54    pub charger: Option<fcharger::Status>,
55    /// Sent as the battery `WatchOptions.wake_on`. `None` wakes the system on no field.
56    pub battery_wake_on: Option<fbattery::Status>,
57    /// Sent as the charger `WatchOptions.wake_on`, like `battery_wake_on`.
58    pub charger_wake_on: Option<fcharger::Status>,
59}
60
61/// Trait implemented by specific charging algorithms (e.g. MSC, thermal, battery defender).
62pub trait Governor: Send + 'static {
63    /// Evaluates the latest telemetry snapshot and returns the desired session vote and optional
64    /// periodic sampling.
65    ///
66    /// May be called more than once per battery-manager update, usually with the same snapshot, so
67    /// a call is not necessarily a new sample.
68    fn evaluate(&mut self, telemetry: &Telemetry) -> Evaluation;
69
70    /// Returns the fields whose changes trigger an evaluation or wake the system. Queried once per
71    /// session.
72    fn watch_interest(&self) -> WatchInterest {
73        WatchInterest::default()
74    }
75}
76
77/// Invokes `$m!(field)` for every `ControlOptions` field a governor can vote on. Merging, clearing,
78/// and diffing votes all use this list. A field missing from it is never sent to the charger (see
79/// [`unforwarded_fields`]), so add each new `ControlOptions` field here.
80macro_rules! for_each_control_field {
81    ($m:ident) => {
82        $m!(operating_mode);
83        $m!(input_current_limit_ua);
84        $m!(charge_current_limit_ua);
85        $m!(float_voltage_uv);
86        $m!(charge_term_current_ua);
87        $m!(input_voltage_limit_uv);
88        $m!(top_off_duration);
89    };
90}
91
92/// Returns the fields of `vote` that `for_each_control_field!` doesn't list, which are never sent.
93fn unforwarded_fields(vote: &fcharger::ControlOptions) -> fcharger::ControlOptions {
94    let mut rest = vote.clone();
95    macro_rules! strip {
96        ($f:ident) => {
97            rest.$f = None;
98        };
99    }
100    for_each_control_field!(strip);
101    rest
102}
103
104/// Copies the `Some` fields of `patch` into `vote`, keeping `vote`'s other fields.
105pub fn merge_vote(vote: &mut fcharger::ControlOptions, patch: &fcharger::ControlOptions) {
106    macro_rules! merge {
107        ($f:ident) => {
108            vote.$f = patch.$f.or(vote.$f);
109        };
110    }
111    for_each_control_field!(merge);
112}
113
114/// Clears the fields selected by `mask` from `vote`.
115pub fn clear_vote_fields(vote: &mut fcharger::ControlOptions, mask: &fcharger::ControlOptions) {
116    macro_rules! clear {
117        ($f:ident) => {
118            if mask.$f.is_some() {
119                vote.$f = None;
120            }
121        };
122    }
123    for_each_control_field!(clear);
124}
125
126/// Computes `(to_set, to_clear)` delta masks between `applied` and `desired`.
127pub fn diff_controls(
128    applied: &fcharger::ControlOptions,
129    desired: &fcharger::ControlOptions,
130) -> (fcharger::ControlOptions, fcharger::ControlOptions) {
131    let mut to_set = fcharger::ControlOptions::default();
132    let mut to_clear = fcharger::ControlOptions::default();
133    // `ClearControl` ignores values, so `to_clear` carries the withdrawn values for logging.
134    macro_rules! diff {
135        ($f:ident) => {
136            to_set.$f = desired.$f.filter(|&v| Some(v) != applied.$f);
137            to_clear.$f = applied.$f.filter(|_| desired.$f.is_none());
138        };
139    }
140    for_each_control_field!(diff);
141    (to_set, to_clear)
142}
143
144/// Evaluates `telemetry` and synchronizes the resulting vote with `controller`.
145///
146/// Fields to set are sent before fields to clear, so that the vote never relaxes before it
147/// restricts. If `SetControl` is rejected, `ClearControl` is skipped so that no field is relaxed
148/// while a new restriction could not be applied. Rejected changes are logged and retried on the
149/// next evaluation, while the session and the votes `controller` holds are kept. `last_accepted`
150/// tracks the vote `controller` accepted. The evaluation is returned even if a change was
151/// rejected, so its sampling still takes effect. Returns `Err` only on FIDL transport failure.
152async fn process_telemetry_event<T: Governor>(
153    governor: &mut T,
154    telemetry: &Telemetry,
155    last_accepted: &mut fcharger::ControlOptions,
156    controller: &fcharger::ControllerProxy,
157) -> Result<Evaluation> {
158    let evaluation = governor.evaluate(telemetry);
159    let unforwarded = unforwarded_fields(&evaluation.vote);
160    if unforwarded != fcharger::ControlOptions::default() {
161        static WARN_UNFORWARDED: std::sync::Once = std::sync::Once::new();
162        WARN_UNFORWARDED
163            .call_once(|| warn!("Ignoring vote fields the harness can't send: {unforwarded:?}"));
164    }
165    let (to_set, to_clear) = diff_controls(last_accepted, &evaluation.vote);
166
167    if to_set != fcharger::ControlOptions::default() {
168        match controller.set_control(&to_set).await.context("Controller.SetControl")? {
169            Ok(()) => merge_vote(last_accepted, &to_set),
170            Err(err) => {
171                warn!("Charger controller rejected SetControl ({to_set:?}): {err:?}");
172                return Ok(evaluation);
173            }
174        }
175    }
176
177    if to_clear != fcharger::ControlOptions::default() {
178        match controller.clear_control(&to_clear).await.context("Controller.ClearControl")? {
179            Ok(()) => clear_vote_fields(last_accepted, &to_clear),
180            Err(err) => {
181                warn!("Charger controller rejected ClearControl ({to_clear:?}): {err:?}");
182            }
183        }
184    }
185
186    Ok(evaluation)
187}
188
189/// The periodic battery sampling a governor votes for.
190#[derive(Clone, Copy, Debug, Default, PartialEq)]
191struct Sampling {
192    /// Longest time between battery statuses, or `None` for no periodic samples.
193    interval: Option<zx::BootDuration>,
194    /// Whether samples wake the system from suspend. Only meaningful with an `interval`.
195    wake: bool,
196}
197
198impl From<&Evaluation> for Sampling {
199    fn from(evaluation: &Evaluation) -> Self {
200        let interval = evaluation.sample_interval;
201        Self { interval, wake: interval.is_some() && evaluation.wake_for_samples }
202    }
203}
204
205/// Configures `Battery.Watch` with the battery fields of `interest`, and votes for battery-manager
206/// to sample the battery as `sampling` asks, or withdraws the vote if `sampling.interval` is
207/// `None`.
208///
209/// `ConfigureWatch` replaces all options, so `interest` is sent with every change in `sampling`.
210/// Rejections are logged rather than retried. Returns `Err` only on FIDL transport failure.
211async fn configure_battery_watch(
212    battery: &fbattery::BatteryProxy,
213    interest: &WatchInterest,
214    sampling: Sampling,
215) -> Result<()> {
216    let options = fbattery::WatchOptions {
217        interest: interest.battery.clone(),
218        wake_on: interest.battery_wake_on.clone(),
219        max_interval: sampling.interval.map(zx::BootDuration::into_nanos),
220        wake_on_max_interval: sampling.wake.then_some(true),
221        ..Default::default()
222    };
223    match battery.configure_watch(&options).await.context("Battery.ConfigureWatch")? {
224        Ok(effective) if effective == options => debug!("Battery watch options set to {options:?}"),
225        Ok(effective) => info!("Battery honors watch options {effective:?} instead of {options:?}"),
226        Err(e) => warn!("Battery.ConfigureWatch rejected {options:?}: {e:?}"),
227    }
228    Ok(())
229}
230
231/// Configures `Charger.Watch` with the charger fields of `interest`.
232///
233/// Rejections are logged rather than retried. Returns `Err` only on FIDL transport failure.
234async fn configure_charger_watch(
235    charger: &fcharger::ChargerProxy,
236    interest: &WatchInterest,
237) -> Result<()> {
238    let options = fcharger::WatchOptions {
239        interest: interest.charger.clone(),
240        wake_on: interest.charger_wake_on.clone(),
241        ..Default::default()
242    };
243    match charger.configure_watch(&options).await.context("Charger.ConfigureWatch")? {
244        Ok(effective) if effective == options => debug!("Charger watch options set to {options:?}"),
245        Ok(effective) => info!("Charger honors watch options {effective:?} instead of {options:?}"),
246        Err(e) => warn!("Charger.ConfigureWatch rejected {options:?}: {e:?}"),
247    }
248    Ok(())
249}
250
251/// Connections to battery-manager for one governor session.
252struct Session {
253    battery: fbattery::BatteryProxy,
254    charger: fcharger::ChargerProxy,
255    controller: fcharger::ControllerProxy,
256}
257
258impl Session {
259    /// Connects to the protocols battery-manager provides in the incoming namespace.
260    fn connect() -> Result<Self> {
261        Ok(Self {
262            battery: connect_to_protocol::<fbattery::BatteryMarker>()?,
263            charger: connect_to_protocol::<fcharger::ChargerMarker>()?,
264            controller: connect_to_protocol::<fcharger::ControllerMarker>()?,
265        })
266    }
267}
268
269/// Records the status of a `Battery.Watch` response in `latest` and returns its wake lease, which
270/// the caller holds until it passes it to the next `Watch`.
271fn battery_response(
272    result: Result<fbattery::BatteryWatchResult, fidl::Error>,
273    latest: &mut Option<fbattery::Status>,
274) -> Result<Option<zx::EventPair>> {
275    let (status, wake_lease) =
276        result.context("Battery.Watch")?.map_err(|e| anyhow!("Battery.Watch failed: {e:?}"))?;
277    *latest = Some(status);
278    Ok(wake_lease)
279}
280
281/// Records the status of a `Charger.Watch` response in `latest` and returns its wake lease, which
282/// the caller holds until it passes it to the next `Watch`.
283fn charger_response(
284    result: Result<fcharger::ChargerWatchResult, fidl::Error>,
285    latest: &mut Option<fcharger::Status>,
286) -> Result<Option<zx::EventPair>> {
287    let (status, wake_lease) =
288        result.context("Charger.Watch")?.map_err(|e| anyhow!("Charger.Watch failed: {e:?}"))?;
289    *latest = Some(status);
290    Ok(wake_lease)
291}
292
293/// Runs the governor event loop over one `session` until a connection fails.
294///
295/// Configures [`Governor::watch_interest`] on both connections before the first `Watch`. Then, on
296/// each update once a battery status is known:
297///
298/// - **Snapshot**: the statuses arrive on separate channels, so a `Watch` response is only a
299///   trigger. If the other response isn't ready, the other status is read with `GetStatus`, which
300///   battery-manager updates before completing either `Watch`. So the governor never evaluates a
301///   new status with a stale one, e.g. when it wakes between battery-manager's responses to one
302///   charger event, and a narrowed charger interest still sees other charger fields, such as the
303///   charge phase, at the next battery sample. The later response re-evaluates, usually the same
304///   snapshot, so [`Governor::evaluate`] may run more than once per update.
305/// - **No charger**: once `Charger.GetStatus` fails with no charger status known, as on a board
306///   without a charger driver, the charger is not read again until `Charger.Watch` reports it.
307/// - **Sampling**: `Evaluation::sample_interval` and `Evaluation::wake_for_samples` are voted
308///   through `Battery.ConfigureWatch` whenever they change. Only the battery is sampled: charger
309///   changes in the governor's interest are notified as they happen, whereas battery fields such
310///   as temperature and voltage are only observed by sampling.
311/// - **Wake leases**: a response's lease is held while the update is handled, then passed to the
312///   next `Watch` on that connection, which hands it back if it completes immediately and drops it
313///   if it is parked.
314async fn run_session<T: Governor>(governor: &mut T, session: &Session) -> Result<Infallible> {
315    let Session { battery, charger, controller } = session;
316    let mut last_accepted = fcharger::ControlOptions::default();
317    // battery-manager starts every connection without a `max_interval` vote.
318    let mut sampling = Sampling::default();
319    let mut latest_battery = None;
320    let mut latest_charger = None;
321    // Whether `Charger.GetStatus` failed before any charger status was known.
322    let mut charger_unavailable = false;
323
324    let interest = governor.watch_interest();
325    if interest.battery.is_some() || interest.battery_wake_on.is_some() {
326        configure_battery_watch(battery, &interest, sampling).await?;
327    }
328    if interest.charger.is_some() || interest.charger_wake_on.is_some() {
329        configure_charger_watch(charger, &interest).await?;
330    }
331
332    let mut battery_watch = battery.watch(None).fuse();
333    let mut charger_watch = charger.watch(None).fuse();
334
335    loop {
336        let mut battery_updated = false;
337        let mut charger_updated = false;
338        let mut battery_lease = None;
339        let mut charger_lease = None;
340        futures::select_biased! {
341            result = battery_watch => {
342                battery_lease = battery_response(result, &mut latest_battery)?;
343                battery_updated = true;
344                if let Some(result) = (&mut charger_watch).now_or_never() {
345                    charger_lease = charger_response(result, &mut latest_charger)?;
346                    charger_updated = true;
347                }
348            }
349            result = charger_watch => {
350                charger_lease = charger_response(result, &mut latest_charger)?;
351                charger_updated = true;
352                if let Some(result) = (&mut battery_watch).now_or_never() {
353                    battery_lease = battery_response(result, &mut latest_battery)?;
354                    battery_updated = true;
355                }
356            }
357        }
358        if charger_updated {
359            charger_unavailable = false;
360        }
361
362        // Read the status that did not trigger this update, so that it is evaluated as of the same
363        // battery-manager update. This also picks up fields outside the governor's interest, such
364        // as the battery current on plug-in, which change without completing `Watch`.
365        // battery-manager re-reads the battery before it publishes a charger update, so the cached
366        // battery status is current. It fails while no battery driver is connected.
367        if !battery_updated {
368            match battery.get_status().await.context("Battery.GetStatus")? {
369                Ok(status) => latest_battery = Some(status),
370                Err(e) => warn!("Battery.GetStatus failed ({e:?}); using the last battery status"),
371            }
372        }
373        if !charger_updated && !charger_unavailable {
374            match charger.get_status().await.context("Charger.GetStatus")? {
375                Ok(status) => latest_charger = Some(status),
376                Err(e) if latest_charger.is_none() => {
377                    debug!("No charger status ({e:?}); not reading it until Charger.Watch reports");
378                    charger_unavailable = true;
379                }
380                Err(e) => warn!("Charger.GetStatus failed ({e:?}); using the last charger status"),
381            }
382        }
383
384        if let Some(battery_status) = &latest_battery {
385            let telemetry =
386                Telemetry { battery: battery_status.clone(), charger: latest_charger.clone() };
387            let evaluation =
388                process_telemetry_event(governor, &telemetry, &mut last_accepted, controller)
389                    .await?;
390            let requested = Sampling::from(&evaluation);
391            if requested != sampling {
392                configure_battery_watch(battery, &interest, requested).await?;
393                sampling = requested;
394            }
395        }
396
397        // Re-arm the hanging gets only after the battery refresh, the vote, and the sampling, so
398        // that the wake leases of the responses keep the system awake across actuation.
399        if battery_updated {
400            battery_watch = battery.watch(battery_lease).fuse();
401        }
402        if charger_updated {
403            charger_watch = charger.watch(charger_lease).fuse();
404        }
405    }
406}
407
408/// Runs sessions obtained from `connect` forever, reconnecting [`RECONNECT_DELAY`] after each
409/// failure.
410async fn supervise<T: Governor>(
411    governor: &mut T,
412    mut connect: impl FnMut() -> Result<Session>,
413) -> Infallible {
414    loop {
415        match connect() {
416            Ok(session) => {
417                let Err(e) = run_session(governor, &session).await;
418                warn!("Governor session ended ({e:?}); reconnecting in {RECONNECT_DELAY:?}");
419            }
420            Err(e) => warn!("Failed to connect to battery-manager ({e:?}); retrying"),
421        }
422        fasync::Timer::new(fasync::MonotonicInstant::after(RECONNECT_DELAY)).await;
423    }
424}
425
426/// Main execution loop for an autonomous charge governor component. Never returns.
427///
428/// Publishes the component's singleton [`fuchsia_inspect::Inspector`] and connects to
429/// `fuchsia.hardware.power.battery.Battery`, `fuchsia.hardware.power.charger.Charger` and
430/// `fuchsia.hardware.power.charger.Controller`, all provided by battery-manager, reconnecting
431/// whenever any of them fails.
432pub async fn run_governor<T: Governor>(mut governor: T) -> Result<()> {
433    let _inspect_server_task = inspect_runtime::publish(
434        fuchsia_inspect::component::inspector(),
435        inspect_runtime::PublishOptions::default(),
436    );
437    match supervise(&mut governor, Session::connect).await {}
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443    use fidl::endpoints::create_proxy_and_stream;
444    use futures::future::LocalBoxFuture;
445    use futures::{Stream, StreamExt, TryStreamExt};
446    use std::cell::RefCell;
447    use std::collections::VecDeque;
448    use std::rc::Rc;
449    use std::sync::{Arc, Mutex};
450    use std::task::Poll;
451
452    const TEST_FCC_HIGH_UA: u32 = 1_500_000;
453    const TEST_FCC_LOW_UA: u32 = 800_000;
454    const TEST_FV_UV: u32 = 4_200_000;
455    const TEST_ICL_UA: u32 = 2_000_000;
456    const TEST_ITERM_UA: u32 = 100_000;
457    const TEST_VINDPM_UV: u32 = 4_600_000;
458    const TEST_TOP_OFF_DURATION_NANOS: i64 = 30_000_000_000;
459    const TEST_VBAT_UV: u32 = 3_900_000;
460    const TEST_VBAT_UPDATED_UV: u32 = 3_950_000;
461    const TEST_SAMPLE_INTERVAL: zx::BootDuration = zx::BootDuration::from_seconds(5);
462    const TEST_LONGER_SAMPLE_INTERVAL: zx::BootDuration = zx::BootDuration::from_seconds(10);
463    const TEST_LONG_IDLE: zx::MonotonicDuration = zx::MonotonicDuration::from_hours(1);
464    const ONE_NANO: zx::MonotonicDuration = zx::MonotonicDuration::from_nanos(1);
465
466    #[derive(Clone, Debug, PartialEq)]
467    enum ControllerCall {
468        Set(fcharger::ControlOptions),
469        Clear(fcharger::ControlOptions),
470    }
471
472    struct SequenceGovernor {
473        votes: Vec<Evaluation>,
474        idx: usize,
475        interest: WatchInterest,
476        /// Every telemetry snapshot passed to `evaluate`, in order.
477        seen: Arc<Mutex<Vec<Telemetry>>>,
478    }
479
480    impl SequenceGovernor {
481        /// Returns `votes` in order, then repeats the last one forever.
482        fn new(votes: Vec<Evaluation>) -> Self {
483            Self::with_interest(votes, WatchInterest::default())
484        }
485
486        fn with_interest(votes: Vec<Evaluation>, interest: WatchInterest) -> Self {
487            Self { votes, idx: 0, interest, seen: Arc::default() }
488        }
489    }
490
491    impl Governor for SequenceGovernor {
492        fn evaluate(&mut self, telemetry: &Telemetry) -> Evaluation {
493            self.seen.lock().unwrap().push(telemetry.clone());
494            let eval = self.votes[self.idx.min(self.votes.len() - 1)].clone();
495            self.idx += 1;
496            eval
497        }
498
499        fn watch_interest(&self) -> WatchInterest {
500            self.interest.clone()
501        }
502    }
503
504    fn battery_status(voltage_uv: u32) -> fbattery::Status {
505        fbattery::Status { voltage_uv: Some(voltage_uv), ..Default::default() }
506    }
507
508    fn fcc_vote(fcc_ua: u32) -> fcharger::ControlOptions {
509        fcharger::ControlOptions { charge_current_limit_ua: Some(fcc_ua), ..Default::default() }
510    }
511
512    /// A vote with every `ControlOptions` field populated.
513    fn full_vote() -> fcharger::ControlOptions {
514        fcharger::ControlOptions {
515            operating_mode: Some(fcharger::OperatingMode::Passthrough),
516            input_current_limit_ua: Some(TEST_ICL_UA),
517            charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
518            float_voltage_uv: Some(TEST_FV_UV),
519            charge_term_current_ua: Some(TEST_ITERM_UA),
520            input_voltage_limit_uv: Some(TEST_VINDPM_UV),
521            top_off_duration: Some(TEST_TOP_OFF_DURATION_NANOS),
522            ..Default::default()
523        }
524    }
525
526    /// Serves `stream`, recording every call and replying with `set_result` / `clear_result`.
527    fn serve_controller(
528        mut stream: fcharger::ControllerRequestStream,
529        set_result: Result<(), fcharger::Error>,
530        clear_result: Result<(), fcharger::Error>,
531    ) -> (fasync::Task<()>, Arc<Mutex<Vec<ControllerCall>>>) {
532        let calls = Arc::new(Mutex::new(Vec::new()));
533        let calls_clone = calls.clone();
534        let task = fasync::Task::local(async move {
535            while let Ok(Some(req)) = stream.try_next().await {
536                match req {
537                    fcharger::ControllerRequest::SetControl { options, responder } => {
538                        calls_clone.lock().unwrap().push(ControllerCall::Set(options));
539                        let _ = responder.send(set_result);
540                    }
541                    fcharger::ControllerRequest::ClearControl { options, responder } => {
542                        calls_clone.lock().unwrap().push(ControllerCall::Clear(options));
543                        let _ = responder.send(clear_result);
544                    }
545                    fcharger::ControllerRequest::_UnknownMethod { .. } => {}
546                }
547            }
548        });
549        (task, calls)
550    }
551
552    /// Verifies that every field a governor votes on is listed in `for_each_control_field!`, so the
553    /// harness sends all of them.
554    #[fuchsia::test]
555    fn test_unforwarded_fields_is_empty_for_every_field() {
556        assert_eq!(unforwarded_fields(&full_vote()), fcharger::ControlOptions::default());
557    }
558
559    /// Verifies that `merge_vote` overwrites only the fields present in `patch` and leaves `None`
560    /// fields in `patch` untouched.
561    #[fuchsia::test]
562    fn test_merge_vote_table() {
563        struct Case {
564            name: &'static str,
565            vote: fcharger::ControlOptions,
566            patch: fcharger::ControlOptions,
567            expected: fcharger::ControlOptions,
568        }
569        let cases = [
570            Case {
571                name: "empty patch leaves vote unchanged",
572                vote: full_vote(),
573                patch: fcharger::ControlOptions::default(),
574                expected: full_vote(),
575            },
576            Case {
577                name: "patch onto empty vote copies patch",
578                vote: fcharger::ControlOptions::default(),
579                patch: full_vote(),
580                expected: full_vote(),
581            },
582            Case {
583                name: "partial patch overrides only its fields",
584                vote: full_vote(),
585                patch: fcharger::ControlOptions {
586                    operating_mode: Some(fcharger::OperatingMode::Charging),
587                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
588                    ..Default::default()
589                },
590                expected: fcharger::ControlOptions {
591                    operating_mode: Some(fcharger::OperatingMode::Charging),
592                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
593                    ..full_vote()
594                },
595            },
596            Case {
597                name: "patch adds new field alongside existing",
598                vote: fcc_vote(TEST_FCC_HIGH_UA),
599                patch: fcharger::ControlOptions {
600                    float_voltage_uv: Some(TEST_FV_UV),
601                    ..Default::default()
602                },
603                expected: fcharger::ControlOptions {
604                    charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
605                    float_voltage_uv: Some(TEST_FV_UV),
606                    ..Default::default()
607                },
608            },
609        ];
610        for Case { name, mut vote, patch, expected } in cases {
611            merge_vote(&mut vote, &patch);
612            assert_eq!(vote, expected, "case: {name}");
613        }
614    }
615
616    /// Verifies that `clear_vote_fields` clears only the fields present in `mask`, regardless of
617    /// the field values inside `mask`.
618    #[fuchsia::test]
619    fn test_clear_vote_fields_table() {
620        struct Case {
621            name: &'static str,
622            vote: fcharger::ControlOptions,
623            mask: fcharger::ControlOptions,
624            expected: fcharger::ControlOptions,
625        }
626        let cases = [
627            Case {
628                name: "empty mask leaves vote unchanged",
629                vote: full_vote(),
630                mask: fcharger::ControlOptions::default(),
631                expected: full_vote(),
632            },
633            Case {
634                name: "full mask clears every field",
635                vote: full_vote(),
636                mask: full_vote(),
637                expected: fcharger::ControlOptions::default(),
638            },
639            Case {
640                name: "mask value is ignored, only presence matters",
641                vote: full_vote(),
642                mask: fcharger::ControlOptions {
643                    operating_mode: Some(fcharger::OperatingMode::Otg),
644                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
645                    ..Default::default()
646                },
647                expected: fcharger::ControlOptions {
648                    operating_mode: None,
649                    charge_current_limit_ua: None,
650                    ..full_vote()
651                },
652            },
653            Case {
654                name: "clearing already-absent field is a no-op",
655                vote: fcc_vote(TEST_FCC_HIGH_UA),
656                mask: fcharger::ControlOptions { float_voltage_uv: Some(0), ..Default::default() },
657                expected: fcc_vote(TEST_FCC_HIGH_UA),
658            },
659        ];
660        for Case { name, mut vote, mask, expected } in cases {
661            clear_vote_fields(&mut vote, &mask);
662            assert_eq!(vote, expected, "case: {name}");
663        }
664    }
665
666    /// Verifies that `diff_controls` partitions changes between `applied` and `desired` into
667    /// `to_set` (added or modified fields) and `to_clear` (withdrawn fields).
668    #[fuchsia::test]
669    fn test_diff_controls_table() {
670        struct Case {
671            name: &'static str,
672            applied: fcharger::ControlOptions,
673            desired: fcharger::ControlOptions,
674            to_set: fcharger::ControlOptions,
675            to_clear: fcharger::ControlOptions,
676        }
677        let cases = [
678            Case {
679                name: "both empty",
680                applied: fcharger::ControlOptions::default(),
681                desired: fcharger::ControlOptions::default(),
682                to_set: fcharger::ControlOptions::default(),
683                to_clear: fcharger::ControlOptions::default(),
684            },
685            Case {
686                name: "identical full vote",
687                applied: full_vote(),
688                desired: full_vote(),
689                to_set: fcharger::ControlOptions::default(),
690                to_clear: fcharger::ControlOptions::default(),
691            },
692            Case {
693                name: "all fields newly set",
694                applied: fcharger::ControlOptions::default(),
695                desired: full_vote(),
696                to_set: full_vote(),
697                to_clear: fcharger::ControlOptions::default(),
698            },
699            Case {
700                name: "all fields withdrawn",
701                applied: full_vote(),
702                desired: fcharger::ControlOptions::default(),
703                to_set: fcharger::ControlOptions::default(),
704                to_clear: full_vote(),
705            },
706            Case {
707                name: "single field changed",
708                applied: full_vote(),
709                desired: fcharger::ControlOptions {
710                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
711                    ..full_vote()
712                },
713                to_set: fcc_vote(TEST_FCC_LOW_UA),
714                to_clear: fcharger::ControlOptions::default(),
715            },
716            Case {
717                name: "operating mode switch is a set",
718                applied: fcharger::ControlOptions {
719                    operating_mode: Some(fcharger::OperatingMode::Passthrough),
720                    ..Default::default()
721                },
722                desired: fcharger::ControlOptions {
723                    operating_mode: Some(fcharger::OperatingMode::Charging),
724                    ..Default::default()
725                },
726                to_set: fcharger::ControlOptions {
727                    operating_mode: Some(fcharger::OperatingMode::Charging),
728                    ..Default::default()
729                },
730                to_clear: fcharger::ControlOptions::default(),
731            },
732            Case {
733                name: "changing a value to zero is a set, not a clear",
734                applied: fcc_vote(TEST_FCC_HIGH_UA),
735                desired: fcc_vote(0),
736                to_set: fcc_vote(0),
737                to_clear: fcharger::ControlOptions::default(),
738            },
739            Case {
740                name: "mixed set and clear",
741                applied: fcharger::ControlOptions {
742                    charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
743                    float_voltage_uv: Some(TEST_FV_UV),
744                    ..Default::default()
745                },
746                desired: fcharger::ControlOptions {
747                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
748                    input_current_limit_ua: Some(TEST_ICL_UA),
749                    ..Default::default()
750                },
751                to_set: fcharger::ControlOptions {
752                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
753                    input_current_limit_ua: Some(TEST_ICL_UA),
754                    ..Default::default()
755                },
756                to_clear: fcharger::ControlOptions {
757                    float_voltage_uv: Some(TEST_FV_UV),
758                    ..Default::default()
759                },
760            },
761        ];
762        for Case { name, applied, desired, to_set, to_clear } in cases {
763            assert_eq!(diff_controls(&applied, &desired), (to_set, to_clear), "case: {name}");
764        }
765    }
766
767    /// Verifies that `process_telemetry_event` sends only changed fields, deduplicates identical
768    /// consecutive votes, and orders `SetControl` before `ClearControl`.
769    #[fuchsia::test]
770    async fn test_process_telemetry_event_sets_and_clears_diffs() {
771        let (controller_proxy, controller_stream) =
772            create_proxy_and_stream::<fcharger::ControllerMarker>();
773        let (_server, calls) = serve_controller(controller_stream, Ok(()), Ok(()));
774
775        let mut gov = SequenceGovernor::new(vec![
776            // 1. Initial vote: FCC + FV + Passthrough
777            Evaluation {
778                vote: fcharger::ControlOptions {
779                    operating_mode: Some(fcharger::OperatingMode::Passthrough),
780                    charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
781                    float_voltage_uv: Some(TEST_FV_UV),
782                    ..Default::default()
783                },
784                sample_interval: Some(TEST_SAMPLE_INTERVAL),
785                ..Default::default()
786            },
787            // 2. Identical vote -> no FIDL calls
788            Evaluation {
789                vote: fcharger::ControlOptions {
790                    operating_mode: Some(fcharger::OperatingMode::Passthrough),
791                    charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
792                    float_voltage_uv: Some(TEST_FV_UV),
793                    ..Default::default()
794                },
795                sample_interval: Some(TEST_SAMPLE_INTERVAL),
796                ..Default::default()
797            },
798            // 3. Lower FCC and clear operating_mode -> SetControl(FCC) + ClearControl(mode)
799            Evaluation {
800                vote: fcharger::ControlOptions {
801                    operating_mode: None,
802                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
803                    float_voltage_uv: Some(TEST_FV_UV),
804                    ..Default::default()
805                },
806                sample_interval: None,
807                ..Default::default()
808            },
809        ]);
810
811        let telemetry = Telemetry::default();
812        let mut last_accepted = fcharger::ControlOptions::default();
813
814        // Step 1: full initial vote
815        let eval1 =
816            process_telemetry_event(&mut gov, &telemetry, &mut last_accepted, &controller_proxy)
817                .await
818                .unwrap();
819        assert_eq!(eval1.sample_interval, Some(TEST_SAMPLE_INTERVAL));
820        assert_eq!(
821            std::mem::take(&mut *calls.lock().unwrap()),
822            vec![ControllerCall::Set(fcharger::ControlOptions {
823                operating_mode: Some(fcharger::OperatingMode::Passthrough),
824                charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
825                float_voltage_uv: Some(TEST_FV_UV),
826                ..Default::default()
827            })]
828        );
829
830        // Step 2: deduplicated -> empty calls
831        let _ =
832            process_telemetry_event(&mut gov, &telemetry, &mut last_accepted, &controller_proxy)
833                .await
834                .unwrap();
835        assert!(calls.lock().unwrap().is_empty());
836
837        // Step 3: SetControl(FCC) + ClearControl(operating_mode)
838        let eval3 =
839            process_telemetry_event(&mut gov, &telemetry, &mut last_accepted, &controller_proxy)
840                .await
841                .unwrap();
842        assert_eq!(eval3.sample_interval, None);
843        assert_eq!(
844            std::mem::take(&mut *calls.lock().unwrap()),
845            vec![
846                ControllerCall::Set(fcharger::ControlOptions {
847                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
848                    ..Default::default()
849                }),
850                ControllerCall::Clear(fcharger::ControlOptions {
851                    operating_mode: Some(fcharger::OperatingMode::Passthrough),
852                    ..Default::default()
853                }),
854            ]
855        );
856    }
857
858    /// Verifies that a domain error from `SetControl` is logged rather than treated as a fatal
859    /// session error, leaves `last_accepted` unchanged, and skips `ClearControl`.
860    #[fuchsia::test]
861    async fn test_process_telemetry_event_rejection_is_not_an_error() {
862        let (controller_proxy, controller_stream) =
863            create_proxy_and_stream::<fcharger::ControllerMarker>();
864        let (_server, calls) =
865            serve_controller(controller_stream, Err(fcharger::Error::InvalidArgs), Ok(()));
866        let mut gov = SequenceGovernor::new(vec![Evaluation {
867            vote: fcc_vote(TEST_FCC_LOW_UA),
868            sample_interval: None,
869            ..Default::default()
870        }]);
871
872        let initial_accepted = fcharger::ControlOptions {
873            charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
874            float_voltage_uv: Some(TEST_FV_UV),
875            ..Default::default()
876        };
877        let mut last_accepted = initial_accepted.clone();
878        let res = process_telemetry_event(
879            &mut gov,
880            &Telemetry::default(),
881            &mut last_accepted,
882            &controller_proxy,
883        )
884        .await;
885        assert!(res.is_ok(), "rejection is not an error: {res:?}");
886        // ClearControl is skipped when SetControl fails, and last_accepted is unchanged.
887        assert_eq!(
888            std::mem::take(&mut *calls.lock().unwrap()),
889            vec![ControllerCall::Set(fcc_vote(TEST_FCC_LOW_UA))]
890        );
891        assert_eq!(last_accepted, initial_accepted);
892    }
893
894    /// Verifies that an initial empty vote sends no `SetControl` or `ClearControl` calls.
895    #[fuchsia::test]
896    async fn test_process_telemetry_event_empty_vote_initializes_without_calls() {
897        let (controller_proxy, controller_stream) =
898            create_proxy_and_stream::<fcharger::ControllerMarker>();
899        let (_server, calls) = serve_controller(controller_stream, Ok(()), Ok(()));
900        let mut gov = SequenceGovernor::new(vec![Evaluation::default()]);
901
902        let mut last_accepted = fcharger::ControlOptions::default();
903        process_telemetry_event(
904            &mut gov,
905            &Telemetry::default(),
906            &mut last_accepted,
907            &controller_proxy,
908        )
909        .await
910        .unwrap();
911
912        assert!(calls.lock().unwrap().is_empty());
913        assert_eq!(last_accepted, fcharger::ControlOptions::default());
914    }
915
916    /// Verifies that `last_accepted` stays in sync with the desired vote across a sequence of
917    /// arbitrary set, modify, and clear transitions when all `Controller` calls succeed.
918    #[fuchsia::test]
919    async fn test_process_telemetry_event_tracks_desired_vote_across_sequence() {
920        let (controller_proxy, controller_stream) =
921            create_proxy_and_stream::<fcharger::ControllerMarker>();
922        let (_server, _calls) = serve_controller(controller_stream, Ok(()), Ok(()));
923        let votes = vec![
924            full_vote(),
925            fcharger::ControlOptions::default(),
926            fcc_vote(TEST_FCC_LOW_UA),
927            full_vote(),
928            fcharger::ControlOptions {
929                operating_mode: Some(fcharger::OperatingMode::Charging),
930                float_voltage_uv: Some(TEST_FV_UV),
931                ..Default::default()
932            },
933            fcharger::ControlOptions::default(),
934        ];
935        let mut gov = SequenceGovernor::new(
936            votes
937                .iter()
938                .cloned()
939                .map(|vote| Evaluation { vote, sample_interval: None, ..Default::default() })
940                .collect(),
941        );
942
943        let mut last_accepted = fcharger::ControlOptions::default();
944        for (step, vote) in votes.into_iter().enumerate() {
945            process_telemetry_event(
946                &mut gov,
947                &Telemetry::default(),
948                &mut last_accepted,
949                &controller_proxy,
950            )
951            .await
952            .unwrap();
953            assert_eq!(last_accepted, vote, "step {step}");
954        }
955    }
956
957    /// Verifies that if `SetControl` succeeds and `ClearControl` is rejected in the same turn,
958    /// `last_accepted` records the accepted `SetControl` while retaining the un-cleared field.
959    #[fuchsia::test]
960    async fn test_process_telemetry_event_clear_rejection_keeps_accepted_set() {
961        let (controller_proxy, controller_stream) =
962            create_proxy_and_stream::<fcharger::ControllerMarker>();
963        let (_server, calls) =
964            serve_controller(controller_stream, Ok(()), Err(fcharger::Error::NotSupported));
965        let mut gov = SequenceGovernor::new(vec![Evaluation {
966            vote: fcc_vote(TEST_FCC_LOW_UA),
967            sample_interval: None,
968            ..Default::default()
969        }]);
970
971        let mut last_accepted = fcharger::ControlOptions {
972            charge_current_limit_ua: Some(TEST_FCC_HIGH_UA),
973            float_voltage_uv: Some(TEST_FV_UV),
974            ..Default::default()
975        };
976        let res = process_telemetry_event(
977            &mut gov,
978            &Telemetry::default(),
979            &mut last_accepted,
980            &controller_proxy,
981        )
982        .await;
983
984        assert!(res.is_ok(), "rejection is not an error: {res:?}");
985        assert_eq!(
986            std::mem::take(&mut *calls.lock().unwrap()),
987            vec![
988                ControllerCall::Set(fcc_vote(TEST_FCC_LOW_UA)),
989                ControllerCall::Clear(fcharger::ControlOptions {
990                    float_voltage_uv: Some(TEST_FV_UV),
991                    ..Default::default()
992                }),
993            ]
994        );
995        // The accepted SetControl is recorded; the rejected clear leaves FV in place.
996        assert_eq!(
997            last_accepted,
998            fcharger::ControlOptions {
999                charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
1000                float_voltage_uv: Some(TEST_FV_UV),
1001                ..Default::default()
1002            }
1003        );
1004    }
1005
1006    /// Verifies that a FIDL transport error on `Controller` is propagated as `Err`.
1007    #[fuchsia::test]
1008    async fn test_process_telemetry_event_propagates_transport_error() {
1009        let (controller_proxy, controller_stream) =
1010            create_proxy_and_stream::<fcharger::ControllerMarker>();
1011        drop(controller_stream);
1012        let mut gov = SequenceGovernor::new(vec![Evaluation {
1013            vote: fcc_vote(TEST_FCC_HIGH_UA),
1014            sample_interval: None,
1015            ..Default::default()
1016        }]);
1017
1018        let mut last_accepted = fcharger::ControlOptions::default();
1019        let res = process_telemetry_event(
1020            &mut gov,
1021            &Telemetry::default(),
1022            &mut last_accepted,
1023            &controller_proxy,
1024        )
1025        .await;
1026
1027        assert!(res.is_err());
1028        assert_eq!(last_accepted, fcharger::ControlOptions::default());
1029    }
1030
1031    /// Verifies that a FIDL transport error on `Controller.ClearControl` is propagated as `Err`.
1032    #[fuchsia::test]
1033    async fn test_process_telemetry_event_clear_propagates_transport_error() {
1034        let (controller_proxy, controller_stream) =
1035            create_proxy_and_stream::<fcharger::ControllerMarker>();
1036        drop(controller_stream);
1037        let mut gov = SequenceGovernor::new(vec![Evaluation::default()]);
1038
1039        let mut last_accepted = fcc_vote(TEST_FCC_HIGH_UA);
1040        let res = process_telemetry_event(
1041            &mut gov,
1042            &Telemetry::default(),
1043            &mut last_accepted,
1044            &controller_proxy,
1045        )
1046        .await;
1047
1048        assert!(res.is_err());
1049        assert_eq!(last_accepted, fcc_vote(TEST_FCC_HIGH_UA));
1050    }
1051
1052    /// Polls `stream` once, returning its next item if one is ready (`None` if pending or closed).
1053    fn poll_next<S: Stream + Unpin>(
1054        executor: &mut fasync::TestExecutor,
1055        stream: &mut S,
1056    ) -> Option<S::Item> {
1057        match executor.run_until_stalled(&mut stream.next()) {
1058            Poll::Ready(item) => item,
1059            Poll::Pending => None,
1060        }
1061    }
1062
1063    /// Server ends of one [`Session`].
1064    struct ServerEnds {
1065        battery: fbattery::BatteryRequestStream,
1066        charger: fcharger::ChargerRequestStream,
1067        controller: fcharger::ControllerRequestStream,
1068    }
1069
1070    fn new_session() -> (Session, ServerEnds) {
1071        let (battery, battery_stream) = create_proxy_and_stream::<fbattery::BatteryMarker>();
1072        let (charger, charger_stream) = create_proxy_and_stream::<fcharger::ChargerMarker>();
1073        let (controller, controller_stream) =
1074            create_proxy_and_stream::<fcharger::ControllerMarker>();
1075        let server = ServerEnds {
1076            battery: battery_stream,
1077            charger: charger_stream,
1078            controller: controller_stream,
1079        };
1080        (Session { battery, charger, controller }, server)
1081    }
1082
1083    /// Drives [`supervise`] on a fake-time executor against in-process fake Battery, Charger, and
1084    /// Controller servers whose requests the test answers by hand.
1085    ///
1086    /// Closing a server requires setting its stream to `None` and dropping any outstanding
1087    /// responders, since they keep the channel alive.
1088    struct LoopHarness {
1089        run: LocalBoxFuture<'static, Infallible>,
1090        battery: Option<fbattery::BatteryRequestStream>,
1091        charger: Option<fcharger::ChargerRequestStream>,
1092        controller: Option<fcharger::ControllerRequestStream>,
1093        /// Server ends of the sessions the governor connected that the test has yet to take.
1094        connected: Rc<RefCell<VecDeque<ServerEnds>>>,
1095        seen: Arc<Mutex<Vec<Telemetry>>>,
1096        // Declared last so it outlives the channels above when the harness is dropped.
1097        executor: fasync::TestExecutor,
1098    }
1099
1100    impl LoopHarness {
1101        /// Starts the governor with the default watch interest and takes its first session.
1102        fn new(votes: Vec<Evaluation>) -> Self {
1103            Self::with_interest(votes, WatchInterest::default())
1104        }
1105
1106        /// Starts the governor with `interest` and takes its first session.
1107        fn with_interest(votes: Vec<Evaluation>, interest: WatchInterest) -> Self {
1108            let executor = fasync::TestExecutor::new_with_fake_time();
1109            let connected: Rc<RefCell<VecDeque<ServerEnds>>> = Rc::default();
1110            let mut governor = SequenceGovernor::with_interest(votes, interest);
1111            let seen = governor.seen.clone();
1112            let sessions = connected.clone();
1113            let run = Box::pin(async move {
1114                let connect = || {
1115                    let (session, server) = new_session();
1116                    sessions.borrow_mut().push_back(server);
1117                    Ok(session)
1118                };
1119                supervise(&mut governor, connect).await
1120            });
1121            let mut h = Self {
1122                run,
1123                battery: None,
1124                charger: None,
1125                controller: None,
1126                connected,
1127                seen,
1128                executor,
1129            };
1130            assert!(h.take_session(), "governor did not connect");
1131            h
1132        }
1133
1134        /// Serves the next session the governor connected, returning whether there was one.
1135        fn take_session(&mut self) -> bool {
1136            self.pump();
1137            let Some(server) = self.connected.borrow_mut().pop_front() else {
1138                return false;
1139            };
1140            self.battery = Some(server.battery);
1141            self.charger = Some(server.charger);
1142            self.controller = Some(server.controller);
1143            true
1144        }
1145
1146        /// Runs the governor until it stalls.
1147        fn pump(&mut self) {
1148            match self.executor.run_until_stalled(&mut self.run) {
1149                Poll::Pending => {}
1150                Poll::Ready(never) => match never {},
1151            }
1152        }
1153
1154        /// Advances fake time by `duration` and fires any expired timers.
1155        fn advance(&mut self, duration: zx::MonotonicDuration) {
1156            self.executor.set_fake_time(self.executor.now() + duration);
1157            let _ = self.executor.wake_expired_timers();
1158        }
1159
1160        /// Expects the current session to have ended, and takes the session the governor
1161        /// reconnects exactly [`RECONNECT_DELAY`] later.
1162        fn expect_reconnect(&mut self) {
1163            self.pump();
1164            self.advance(RECONNECT_DELAY - ONE_NANO);
1165            assert!(!self.take_session(), "governor reconnected early");
1166            self.advance(ONE_NANO);
1167            assert!(self.take_session(), "governor did not reconnect");
1168        }
1169
1170        fn next_battery_request(&mut self) -> Option<fbattery::BatteryRequest> {
1171            self.pump();
1172            let battery = self.battery.as_mut().expect("battery closed");
1173            poll_next(&mut self.executor, battery).map(|r| r.expect("battery request"))
1174        }
1175
1176        fn next_charger_request(&mut self) -> Option<fcharger::ChargerRequest> {
1177            self.pump();
1178            let charger = self.charger.as_mut().expect("charger closed");
1179            poll_next(&mut self.executor, charger).map(|r| r.expect("charger request"))
1180        }
1181
1182        fn next_controller_request(&mut self) -> Option<fcharger::ControllerRequest> {
1183            self.pump();
1184            let controller = self.controller.as_mut().expect("controller closed");
1185            poll_next(&mut self.executor, controller).map(|r| r.expect("controller request"))
1186        }
1187
1188        /// Expects `Battery.Watch` without a lease.
1189        fn expect_battery_watch(&mut self) -> fbattery::BatteryWatchResponder {
1190            let (lease, responder) = self.expect_battery_watch_with_lease();
1191            assert!(lease.is_none());
1192            responder
1193        }
1194
1195        fn expect_battery_watch_with_lease(
1196            &mut self,
1197        ) -> (Option<zx::EventPair>, fbattery::BatteryWatchResponder) {
1198            match self.next_battery_request() {
1199                Some(fbattery::BatteryRequest::Watch { lease, responder }) => (lease, responder),
1200                other => panic!("expected Battery.Watch, got {other:?}"),
1201            }
1202        }
1203
1204        fn expect_battery_configure_watch(
1205            &mut self,
1206        ) -> (fbattery::WatchOptions, fbattery::BatteryConfigureWatchResponder) {
1207            match self.next_battery_request() {
1208                Some(fbattery::BatteryRequest::ConfigureWatch { options, responder }) => {
1209                    (options, responder)
1210                }
1211                other => panic!("expected Battery.ConfigureWatch, got {other:?}"),
1212            }
1213        }
1214
1215        /// Expects `Charger.ConfigureWatch`, and accepts it.
1216        fn accept_charger_configure_watch(&mut self) -> fcharger::WatchOptions {
1217            match self.next_charger_request() {
1218                Some(fcharger::ChargerRequest::ConfigureWatch { options, responder }) => {
1219                    responder.send(Ok(&options)).unwrap();
1220                    options
1221                }
1222                other => panic!("expected Charger.ConfigureWatch, got {other:?}"),
1223            }
1224        }
1225
1226        fn expect_battery_get_status(&mut self) -> fbattery::BatteryGetStatusResponder {
1227            match self.next_battery_request() {
1228                Some(fbattery::BatteryRequest::GetStatus { responder }) => responder,
1229                other => panic!("expected Battery.GetStatus, got {other:?}"),
1230            }
1231        }
1232
1233        fn expect_charger_get_status(&mut self) -> fcharger::ChargerGetStatusResponder {
1234            match self.next_charger_request() {
1235                Some(fcharger::ChargerRequest::GetStatus { responder }) => responder,
1236                other => panic!("expected Charger.GetStatus, got {other:?}"),
1237            }
1238        }
1239
1240        /// Expects `Charger.Watch` without a lease.
1241        fn expect_charger_watch(&mut self) -> fcharger::ChargerWatchResponder {
1242            let (lease, responder) = self.expect_charger_watch_with_lease();
1243            assert!(lease.is_none());
1244            responder
1245        }
1246
1247        fn expect_charger_watch_with_lease(
1248            &mut self,
1249        ) -> (Option<zx::EventPair>, fcharger::ChargerWatchResponder) {
1250            match self.next_charger_request() {
1251                Some(fcharger::ChargerRequest::Watch { lease, responder }) => (lease, responder),
1252                other => panic!("expected Charger.Watch, got {other:?}"),
1253            }
1254        }
1255
1256        /// Expects `Battery.ConfigureWatch` with `max_interval` set to `interval`, and accepts it.
1257        fn accept_sample_interval(&mut self, interval: Option<zx::BootDuration>) {
1258            let max_interval = interval.map(zx::BootDuration::into_nanos);
1259            let (options, responder) = self.expect_battery_configure_watch();
1260            assert_eq!(options, fbattery::WatchOptions { max_interval, ..Default::default() });
1261            responder.send(Ok(&options)).unwrap();
1262        }
1263
1264        /// Expects a `SetControl` request, returning its options after accepting it.
1265        fn accept_set_control(&mut self) -> fcharger::ControlOptions {
1266            let (options, responder) = self.expect_set_control();
1267            responder.send(Ok(())).unwrap();
1268            options
1269        }
1270
1271        fn expect_set_control(
1272            &mut self,
1273        ) -> (fcharger::ControlOptions, fcharger::ControllerSetControlResponder) {
1274            match self.next_controller_request() {
1275                Some(fcharger::ControllerRequest::SetControl { options, responder }) => {
1276                    (options, responder)
1277                }
1278                other => panic!("expected Controller.SetControl, got {other:?}"),
1279            }
1280        }
1281
1282        fn expect_clear_control(
1283            &mut self,
1284        ) -> (fcharger::ControlOptions, fcharger::ControllerClearControlResponder) {
1285            match self.next_controller_request() {
1286                Some(fcharger::ControllerRequest::ClearControl { options, responder }) => {
1287                    (options, responder)
1288                }
1289                other => panic!("expected Controller.ClearControl, got {other:?}"),
1290            }
1291        }
1292
1293        fn assert_no_battery_request(&mut self) {
1294            if let Some(req) = self.next_battery_request() {
1295                panic!("unexpected battery request: {req:?}");
1296            }
1297        }
1298
1299        fn assert_no_charger_request(&mut self) {
1300            if let Some(req) = self.next_charger_request() {
1301                panic!("unexpected charger request: {req:?}");
1302            }
1303        }
1304
1305        fn assert_no_controller_request(&mut self) {
1306            if let Some(req) = self.next_controller_request() {
1307                panic!("unexpected controller request: {req:?}");
1308            }
1309        }
1310
1311        fn evaluation_count(&self) -> usize {
1312            self.seen.lock().unwrap().len()
1313        }
1314
1315        fn last_telemetry(&self) -> Telemetry {
1316            self.seen.lock().unwrap().last().cloned().expect("governor never evaluated")
1317        }
1318
1319        /// Answers `Charger.GetStatus` as battery-manager does while no charger status is known.
1320        fn answer_no_charger(&mut self) {
1321            self.expect_charger_get_status().send(Err(fcharger::Error::Io)).unwrap();
1322        }
1323
1324        /// Answers the initial `Battery.Watch` with `voltage_uv` and the resulting
1325        /// `Charger.GetStatus` as if there were no charger, accepts the initial `SetControl`, and
1326        /// returns the re-armed (still pending) `Battery.Watch` responder.
1327        ///
1328        /// The governor must not request a sample interval on its first evaluation.
1329        fn complete_initial_battery_update(
1330            &mut self,
1331            voltage_uv: u32,
1332        ) -> fbattery::BatteryWatchResponder {
1333            let watch = self.expect_battery_watch();
1334            watch.send(Ok((&battery_status(voltage_uv), None))).unwrap();
1335            self.answer_no_charger();
1336            let _ = self.accept_set_control();
1337            self.expect_battery_watch()
1338        }
1339    }
1340
1341    /// Verifies that `Battery.Watch` completions drive governor evaluations and send the resulting
1342    /// `SetControl` / `ClearControl` diffs before re-arming `Battery.Watch`.
1343    #[fuchsia::test]
1344    fn test_loop_battery_watch_drives_votes() {
1345        let mut h = LoopHarness::new(vec![
1346            Evaluation { vote: full_vote(), sample_interval: None, ..Default::default() },
1347            // Same vote -> no controller traffic.
1348            Evaluation { vote: full_vote(), sample_interval: None, ..Default::default() },
1349            // Lower FCC and withdraw FV.
1350            Evaluation {
1351                vote: fcharger::ControlOptions {
1352                    charge_current_limit_ua: Some(TEST_FCC_LOW_UA),
1353                    float_voltage_uv: None,
1354                    ..full_vote()
1355                },
1356                sample_interval: None,
1357                ..Default::default()
1358            },
1359        ]);
1360
1361        let _pending_charger_watch = h.expect_charger_watch();
1362        let watch = h.expect_battery_watch();
1363        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1364        h.answer_no_charger();
1365        assert_eq!(h.accept_set_control(), full_vote());
1366        assert_eq!(
1367            h.last_telemetry(),
1368            Telemetry { battery: battery_status(TEST_VBAT_UV), charger: None }
1369        );
1370
1371        let watch = h.expect_battery_watch();
1372        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1373        h.assert_no_controller_request();
1374        assert_eq!(h.evaluation_count(), 2);
1375        assert_eq!(h.last_telemetry().battery, battery_status(TEST_VBAT_UPDATED_UV));
1376
1377        let watch = h.expect_battery_watch();
1378        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1379        assert_eq!(h.accept_set_control(), fcc_vote(TEST_FCC_LOW_UA));
1380        let (clear, responder) = h.expect_clear_control();
1381        assert_eq!(
1382            clear,
1383            fcharger::ControlOptions { float_voltage_uv: Some(TEST_FV_UV), ..Default::default() }
1384        );
1385        responder.send(Ok(())).unwrap();
1386
1387        let _pending_watch = h.expect_battery_watch();
1388        assert_eq!(h.evaluation_count(), 3);
1389    }
1390
1391    /// Whether every handle to the lease whose peer is `lease_peer` has been closed.
1392    fn is_released(lease_peer: &zx::EventPair) -> bool {
1393        matches!(
1394            lease_peer
1395                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST),
1396            zx::WaitResult::Ok(_)
1397        )
1398    }
1399
1400    /// Verifies that the governor holds the wake lease of a `Watch` response while it actuates
1401    /// the vote, and then passes that same lease to the next `Watch` on the connection.
1402    #[fuchsia::test]
1403    fn test_loop_passes_response_lease_to_next_watch() {
1404        let mut h = LoopHarness::new(vec![Evaluation {
1405            vote: fcc_vote(TEST_FCC_HIGH_UA),
1406            ..Default::default()
1407        }]);
1408        let _pending_charger_watch = h.expect_charger_watch();
1409
1410        let (lease, lease_peer) = zx::EventPair::create();
1411        let lease_koid = lease.koid().unwrap();
1412        let watch = h.expect_battery_watch();
1413        watch.send(Ok((&battery_status(TEST_VBAT_UV), Some(lease)))).unwrap();
1414        h.answer_no_charger();
1415
1416        let (_, set_responder) = h.expect_set_control();
1417        assert!(!is_released(&lease_peer));
1418        set_responder.send(Ok(())).unwrap();
1419
1420        let (setup_lease, _pending_watch) = h.expect_battery_watch_with_lease();
1421        let setup_lease = setup_lease.expect("the response's lease is passed to the next Watch");
1422        assert_eq!(setup_lease.koid().unwrap(), lease_koid);
1423        drop(setup_lease);
1424        assert!(is_released(&lease_peer));
1425    }
1426
1427    /// Verifies that `Evaluation::wake_for_samples` is voted as `wake_on_max_interval` with the
1428    /// sample interval, and withdrawn when the governor stops asking for it.
1429    #[fuchsia::test]
1430    fn test_loop_votes_wake_for_samples() {
1431        let mut h = LoopHarness::new(vec![
1432            Evaluation {
1433                vote: fcc_vote(TEST_FCC_HIGH_UA),
1434                sample_interval: Some(TEST_SAMPLE_INTERVAL),
1435                wake_for_samples: true,
1436            },
1437            Evaluation {
1438                vote: fcc_vote(TEST_FCC_HIGH_UA),
1439                sample_interval: Some(TEST_SAMPLE_INTERVAL),
1440                wake_for_samples: false,
1441            },
1442        ]);
1443        let _pending_charger_watch = h.expect_charger_watch();
1444
1445        let watch = h.expect_battery_watch();
1446        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1447        h.answer_no_charger();
1448        let _ = h.accept_set_control();
1449        let (options, responder) = h.expect_battery_configure_watch();
1450        assert_eq!(
1451            options,
1452            fbattery::WatchOptions {
1453                max_interval: Some(TEST_SAMPLE_INTERVAL.into_nanos()),
1454                wake_on_max_interval: Some(true),
1455                ..Default::default()
1456            }
1457        );
1458        responder.send(Ok(&options)).unwrap();
1459
1460        let watch = h.expect_battery_watch();
1461        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1462        h.accept_sample_interval(Some(TEST_SAMPLE_INTERVAL));
1463        let _pending_watch = h.expect_battery_watch();
1464    }
1465
1466    /// Verifies that `WatchInterest::charger_wake_on` is configured as the charger connection's
1467    /// `wake_on` before the first `Watch`.
1468    #[fuchsia::test]
1469    fn test_loop_configures_charger_wake_on() {
1470        let wake_on = fcharger::Status { online: Some(true), ..Default::default() };
1471        let mut h = LoopHarness::with_interest(
1472            vec![Evaluation { vote: fcc_vote(TEST_FCC_HIGH_UA), ..Default::default() }],
1473            WatchInterest { charger_wake_on: Some(wake_on.clone()), ..Default::default() },
1474        );
1475        assert_eq!(
1476            h.accept_charger_configure_watch(),
1477            fcharger::WatchOptions { wake_on: Some(wake_on), ..Default::default() }
1478        );
1479        let _pending_charger_watch = h.expect_charger_watch();
1480    }
1481
1482    /// Verifies that `Battery.Watch` is re-armed only after `Controller` and
1483    /// `Battery.ConfigureWatch` calls complete, so battery-manager's wake lease remains held
1484    /// across actuation.
1485    #[fuchsia::test]
1486    fn test_loop_rearms_watch_only_after_actuation() {
1487        let mut h = LoopHarness::new(vec![Evaluation {
1488            vote: fcc_vote(TEST_FCC_HIGH_UA),
1489            sample_interval: Some(TEST_SAMPLE_INTERVAL),
1490            ..Default::default()
1491        }]);
1492        let _pending_charger_watch = h.expect_charger_watch();
1493
1494        let watch = h.expect_battery_watch();
1495        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1496        h.answer_no_charger();
1497
1498        // While the vote and the interval are in flight, Battery.Watch is not re-armed yet.
1499        let (_, set_responder) = h.expect_set_control();
1500        h.assert_no_battery_request();
1501        set_responder.send(Ok(())).unwrap();
1502        let (options, battery_responder) = h.expect_battery_configure_watch();
1503        h.assert_no_battery_request();
1504        battery_responder.send(Ok(&options)).unwrap();
1505
1506        let _pending_watch = h.expect_battery_watch();
1507    }
1508
1509    /// Verifies that `Battery.ConfigureWatch` is called only when `Evaluation::sample_interval`
1510    /// changes, and that withdrawing the interval sends `max_interval: None`.
1511    #[fuchsia::test]
1512    fn test_loop_configures_sample_interval_on_change() {
1513        let mut h = LoopHarness::new(vec![
1514            Evaluation {
1515                vote: fcc_vote(TEST_FCC_HIGH_UA),
1516                sample_interval: Some(TEST_SAMPLE_INTERVAL),
1517                ..Default::default()
1518            },
1519            // Unchanged interval -> not sent again.
1520            Evaluation {
1521                vote: fcc_vote(TEST_FCC_HIGH_UA),
1522                sample_interval: Some(TEST_SAMPLE_INTERVAL),
1523                ..Default::default()
1524            },
1525            Evaluation {
1526                vote: fcc_vote(TEST_FCC_HIGH_UA),
1527                sample_interval: Some(TEST_LONGER_SAMPLE_INTERVAL),
1528                ..Default::default()
1529            },
1530            // No interval -> the vote is withdrawn.
1531            Evaluation {
1532                vote: fcc_vote(TEST_FCC_HIGH_UA),
1533                sample_interval: None,
1534                ..Default::default()
1535            },
1536            Evaluation {
1537                vote: fcc_vote(TEST_FCC_HIGH_UA),
1538                sample_interval: None,
1539                ..Default::default()
1540            },
1541        ]);
1542        let _pending_charger_watch = h.expect_charger_watch();
1543
1544        let watch = h.expect_battery_watch();
1545        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1546        h.answer_no_charger();
1547        let _ = h.accept_set_control();
1548        h.accept_sample_interval(Some(TEST_SAMPLE_INTERVAL));
1549
1550        // battery-manager owns sampling: nothing happens locally when the interval elapses.
1551        let watch = h.expect_battery_watch();
1552        h.advance(TEST_LONG_IDLE);
1553        h.assert_no_battery_request();
1554        assert_eq!(h.evaluation_count(), 1);
1555
1556        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1557        let watch = h.expect_battery_watch();
1558        assert_eq!(h.evaluation_count(), 2);
1559
1560        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1561        h.accept_sample_interval(Some(TEST_LONGER_SAMPLE_INTERVAL));
1562
1563        let watch = h.expect_battery_watch();
1564        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1565        h.accept_sample_interval(None);
1566
1567        let watch = h.expect_battery_watch();
1568        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1569        let _pending_watch = h.expect_battery_watch();
1570        assert_eq!(h.evaluation_count(), 5);
1571        h.assert_no_controller_request();
1572        h.assert_no_charger_request();
1573    }
1574
1575    /// Verifies that a declared watch interest is configured on both connections before the first
1576    /// `Watch`, sent again with every sample interval, and configured again on a new session.
1577    #[fuchsia::test]
1578    fn test_loop_configures_watch_interest() {
1579        let battery_interest = fbattery::Status { present: Some(true), ..Default::default() };
1580        let charger_interest = fcharger::Status { online: Some(true), ..Default::default() };
1581        let mut h = LoopHarness::with_interest(
1582            vec![Evaluation {
1583                vote: fcc_vote(TEST_FCC_HIGH_UA),
1584                sample_interval: Some(TEST_SAMPLE_INTERVAL),
1585                ..Default::default()
1586            }],
1587            WatchInterest {
1588                battery: Some(battery_interest.clone()),
1589                charger: Some(charger_interest.clone()),
1590                ..Default::default()
1591            },
1592        );
1593        let interest_only = fbattery::WatchOptions {
1594            interest: Some(battery_interest.clone()),
1595            ..Default::default()
1596        };
1597        let with_interval = fbattery::WatchOptions {
1598            max_interval: Some(TEST_SAMPLE_INTERVAL.into_nanos()),
1599            ..interest_only.clone()
1600        };
1601        let charger_options =
1602            fcharger::WatchOptions { interest: Some(charger_interest), ..Default::default() };
1603
1604        for _ in 0..2 {
1605            let (options, responder) = h.expect_battery_configure_watch();
1606            assert_eq!(options, interest_only);
1607            responder.send(Ok(&options)).unwrap();
1608            assert_eq!(h.accept_charger_configure_watch(), charger_options);
1609
1610            let charger_watch = h.expect_charger_watch();
1611            let watch = h.expect_battery_watch();
1612            watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1613            h.answer_no_charger();
1614            let _ = h.accept_set_control();
1615            let (options, responder) = h.expect_battery_configure_watch();
1616            assert_eq!(options, with_interval);
1617            responder.send(Ok(&options)).unwrap();
1618            let _pending_watch = h.expect_battery_watch();
1619
1620            drop(charger_watch);
1621            h.charger = None;
1622            h.expect_reconnect();
1623        }
1624    }
1625
1626    /// Verifies that a domain error from `Charger.ConfigureWatch` does not end the session.
1627    #[fuchsia::test]
1628    fn test_loop_watch_interest_rejection_is_not_fatal() {
1629        let mut h = LoopHarness::with_interest(
1630            vec![Evaluation {
1631                vote: fcc_vote(TEST_FCC_HIGH_UA),
1632                sample_interval: None,
1633                ..Default::default()
1634            }],
1635            WatchInterest {
1636                charger: Some(fcharger::Status { online: Some(true), ..Default::default() }),
1637                ..Default::default()
1638            },
1639        );
1640        match h.next_charger_request() {
1641            Some(fcharger::ChargerRequest::ConfigureWatch { responder, .. }) => {
1642                responder.send(Err(fcharger::Error::NotSupported)).unwrap()
1643            }
1644            other => panic!("expected Charger.ConfigureWatch, got {other:?}"),
1645        }
1646
1647        let _pending_charger_watch = h.expect_charger_watch();
1648        let _pending_battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1649        assert_eq!(h.evaluation_count(), 1);
1650    }
1651
1652    /// Verifies that a domain error from `Battery.ConfigureWatch` does not end the session or
1653    /// trigger retries when `sample_interval` is unchanged.
1654    #[fuchsia::test]
1655    fn test_loop_sample_interval_rejection_is_not_fatal_or_retried() {
1656        let mut h = LoopHarness::new(vec![Evaluation {
1657            vote: fcc_vote(TEST_FCC_HIGH_UA),
1658            sample_interval: Some(TEST_SAMPLE_INTERVAL),
1659            ..Default::default()
1660        }]);
1661        let _pending_charger_watch = h.expect_charger_watch();
1662
1663        let watch = h.expect_battery_watch();
1664        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1665        h.answer_no_charger();
1666        let _ = h.accept_set_control();
1667        let (_, responder) = h.expect_battery_configure_watch();
1668        responder.send(Err(fbattery::Error::NotSupported)).unwrap();
1669
1670        let watch = h.expect_battery_watch();
1671        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1672        let _pending_watch = h.expect_battery_watch();
1673        assert_eq!(h.evaluation_count(), 2);
1674        h.assert_no_charger_request();
1675    }
1676
1677    /// Verifies that a `Charger.Watch` completion refreshes the battery status via
1678    /// `Battery.GetStatus` before re-arming `Charger.Watch`, then evaluates with both statuses.
1679    #[fuchsia::test]
1680    fn test_loop_charger_watch_refreshes_battery_status() {
1681        let mut h = LoopHarness::new(vec![Evaluation {
1682            vote: fcc_vote(TEST_FCC_HIGH_UA),
1683            sample_interval: None,
1684            ..Default::default()
1685        }]);
1686        let charger_watch = h.expect_charger_watch();
1687        let _pending_battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1688        assert_eq!(h.last_telemetry().charger, None);
1689        let online_charger = fcharger::Status { online: Some(true), ..Default::default() };
1690
1691        charger_watch.send(Ok((&online_charger, None))).unwrap();
1692        // While the battery refresh is in flight, Charger.Watch is not re-armed.
1693        let get_status = h.expect_battery_get_status();
1694        h.assert_no_charger_request();
1695        get_status.send(Ok(&battery_status(TEST_VBAT_UPDATED_UV))).unwrap();
1696
1697        let _pending_charger_watch = h.expect_charger_watch();
1698        h.assert_no_battery_request();
1699        h.assert_no_controller_request();
1700        assert_eq!(h.evaluation_count(), 2);
1701        assert_eq!(
1702            h.last_telemetry(),
1703            Telemetry {
1704                battery: battery_status(TEST_VBAT_UPDATED_UV),
1705                charger: Some(online_charger)
1706            }
1707        );
1708    }
1709
1710    /// Starts a governor whose charger interest is `online`, and completes its initial battery
1711    /// and charger updates, the latter with `charger_status`. Returns the harness and the pending
1712    /// `Battery.Watch` and `Charger.Watch` responders.
1713    fn start_with_online_charger_interest(
1714        charger_status: &fcharger::Status,
1715    ) -> (LoopHarness, fbattery::BatteryWatchResponder, fcharger::ChargerWatchResponder) {
1716        let mut h = LoopHarness::with_interest(
1717            vec![Evaluation {
1718                vote: fcc_vote(TEST_FCC_HIGH_UA),
1719                sample_interval: None,
1720                ..Default::default()
1721            }],
1722            WatchInterest {
1723                charger: Some(fcharger::Status { online: Some(true), ..Default::default() }),
1724                ..Default::default()
1725            },
1726        );
1727        let _ = h.accept_charger_configure_watch();
1728        let charger_watch = h.expect_charger_watch();
1729        let battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1730        charger_watch.send(Ok((charger_status, None))).unwrap();
1731        h.expect_battery_get_status().send(Ok(&battery_status(TEST_VBAT_UV))).unwrap();
1732        let charger_watch = h.expect_charger_watch();
1733        (h, battery_watch, charger_watch)
1734    }
1735
1736    /// Verifies that a `Battery.Watch` update reads the charger status via `Charger.GetStatus`, so
1737    /// that a governor that narrowed its charger interest sees charger fields outside it, such as
1738    /// the charge phase, at the next sample.
1739    #[fuchsia::test]
1740    fn test_loop_battery_watch_refreshes_narrowed_charger_status() {
1741        let fast = fcharger::Status {
1742            online: Some(true),
1743            charge_phase: Some(fcharger::ChargePhase::Fast),
1744            ..Default::default()
1745        };
1746        let (mut h, battery_watch, _pending_charger_watch) =
1747            start_with_online_charger_interest(&fast);
1748
1749        battery_watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1750        // While the charger refresh is in flight, Battery.Watch is not re-armed.
1751        let get_status = h.expect_charger_get_status();
1752        h.assert_no_battery_request();
1753        let taper = fcharger::Status { charge_phase: Some(fcharger::ChargePhase::Taper), ..fast };
1754        get_status.send(Ok(&taper)).unwrap();
1755
1756        let _pending_battery_watch = h.expect_battery_watch();
1757        assert_eq!(h.evaluation_count(), 3);
1758        assert_eq!(
1759            h.last_telemetry(),
1760            Telemetry { battery: battery_status(TEST_VBAT_UPDATED_UV), charger: Some(taper) }
1761        );
1762    }
1763
1764    /// Verifies that if `Charger.GetStatus` returns a domain error after a `Battery.Watch` update,
1765    /// the governor evaluates using the last known charger status.
1766    #[fuchsia::test]
1767    fn test_loop_charger_refresh_error_uses_last_charger_status() {
1768        let online = fcharger::Status { online: Some(true), ..Default::default() };
1769        let (mut h, battery_watch, _pending_charger_watch) =
1770            start_with_online_charger_interest(&online);
1771
1772        battery_watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1773        h.expect_charger_get_status().send(Err(fcharger::Error::Io)).unwrap();
1774        let _pending_battery_watch = h.expect_battery_watch();
1775        assert_eq!(h.evaluation_count(), 3);
1776        assert_eq!(
1777            h.last_telemetry(),
1778            Telemetry { battery: battery_status(TEST_VBAT_UPDATED_UV), charger: Some(online) }
1779        );
1780    }
1781
1782    /// Verifies that if `Battery.GetStatus` returns a domain error after a `Charger.Watch` update,
1783    /// the governor evaluates using the last known battery status.
1784    #[fuchsia::test]
1785    fn test_loop_battery_refresh_error_uses_last_battery_status() {
1786        let mut h = LoopHarness::new(vec![Evaluation {
1787            vote: fcc_vote(TEST_FCC_HIGH_UA),
1788            sample_interval: None,
1789            ..Default::default()
1790        }]);
1791        let charger_watch = h.expect_charger_watch();
1792        let _pending_battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1793        let online_charger = fcharger::Status { online: Some(true), ..Default::default() };
1794
1795        charger_watch.send(Ok((&online_charger, None))).unwrap();
1796        // battery-manager returns IO while no battery driver is connected.
1797        h.expect_battery_get_status().send(Err(fbattery::Error::Io)).unwrap();
1798        let _pending_charger_watch = h.expect_charger_watch();
1799        assert_eq!(h.evaluation_count(), 2);
1800        assert_eq!(
1801            h.last_telemetry(),
1802            Telemetry { battery: battery_status(TEST_VBAT_UV), charger: Some(online_charger) }
1803        );
1804    }
1805
1806    /// Verifies that a transport error during `Battery.GetStatus` ends the session and reconnects.
1807    #[fuchsia::test]
1808    fn test_loop_battery_refresh_transport_error_reconnects() {
1809        let mut h = LoopHarness::new(vec![Evaluation {
1810            vote: fcc_vote(TEST_FCC_HIGH_UA),
1811            sample_interval: None,
1812            ..Default::default()
1813        }]);
1814        let charger_watch = h.expect_charger_watch();
1815        let battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1816
1817        charger_watch.send(Ok((&fcharger::Status::default(), None))).unwrap();
1818        let get_status = h.expect_battery_get_status();
1819        drop((battery_watch, get_status));
1820        h.battery = None;
1821        h.expect_reconnect();
1822        assert_eq!(h.evaluation_count(), 1);
1823    }
1824
1825    /// Verifies that if `Charger.Watch` completes before any battery status is available,
1826    /// evaluation waits until the first battery status arrives.
1827    #[fuchsia::test]
1828    fn test_loop_charger_before_battery_waits_for_battery() {
1829        let mut h = LoopHarness::new(vec![Evaluation {
1830            vote: fcc_vote(TEST_FCC_HIGH_UA),
1831            sample_interval: None,
1832            ..Default::default()
1833        }]);
1834        let battery_watch = h.expect_battery_watch();
1835        let charger_watch = h.expect_charger_watch();
1836        let online_charger = fcharger::Status { online: Some(true), ..Default::default() };
1837
1838        // Charger reports first, before any battery status: nothing to evaluate yet.
1839        charger_watch.send(Ok((&online_charger, None))).unwrap();
1840        h.expect_battery_get_status().send(Err(fbattery::Error::Io)).unwrap();
1841        let _pending_charger_watch = h.expect_charger_watch();
1842        h.assert_no_controller_request();
1843        assert_eq!(h.evaluation_count(), 0);
1844
1845        battery_watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1846        h.expect_charger_get_status().send(Ok(&online_charger)).unwrap();
1847        assert_eq!(h.accept_set_control(), fcc_vote(TEST_FCC_HIGH_UA));
1848        assert_eq!(
1849            h.last_telemetry(),
1850            Telemetry { battery: battery_status(TEST_VBAT_UV), charger: Some(online_charger) }
1851        );
1852    }
1853
1854    /// Verifies that `Battery.Watch` updates are evaluated with `charger: None` even when
1855    /// `Charger.Watch` never completes (e.g. on a board without a charger driver).
1856    #[fuchsia::test]
1857    fn test_loop_pending_charger_watch_does_not_block_battery() {
1858        let mut h = LoopHarness::new(vec![
1859            Evaluation {
1860                vote: fcc_vote(TEST_FCC_HIGH_UA),
1861                sample_interval: None,
1862                ..Default::default()
1863            },
1864            Evaluation {
1865                vote: fcc_vote(TEST_FCC_LOW_UA),
1866                sample_interval: None,
1867                ..Default::default()
1868            },
1869        ]);
1870        // As on a board without a charger driver, Charger.Watch never completes.
1871        let _pending_charger_watch = h.expect_charger_watch();
1872        let battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1873
1874        battery_watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1875        assert_eq!(h.accept_set_control(), fcc_vote(TEST_FCC_LOW_UA));
1876        let _pending_battery_watch = h.expect_battery_watch();
1877        assert_eq!(
1878            h.last_telemetry(),
1879            Telemetry { battery: battery_status(TEST_VBAT_UPDATED_UV), charger: None }
1880        );
1881        h.assert_no_charger_request();
1882    }
1883
1884    /// Verifies that a domain error from `Battery.Watch` ends the session and reconnects after
1885    /// `RECONNECT_DELAY`.
1886    #[fuchsia::test]
1887    fn test_loop_battery_watch_error_reconnects() {
1888        let mut h = LoopHarness::new(vec![Evaluation {
1889            vote: fcc_vote(TEST_FCC_HIGH_UA),
1890            sample_interval: None,
1891            ..Default::default()
1892        }]);
1893        let watch = h.expect_battery_watch();
1894        watch.send(Err(fbattery::Error::AlreadyWatching)).unwrap();
1895        h.expect_reconnect();
1896        assert_eq!(h.evaluation_count(), 0);
1897    }
1898
1899    /// Verifies that a domain error from `Charger.Watch` ends the session and reconnects after
1900    /// `RECONNECT_DELAY`.
1901    #[fuchsia::test]
1902    fn test_loop_charger_watch_error_reconnects() {
1903        let mut h = LoopHarness::new(vec![Evaluation {
1904            vote: fcc_vote(TEST_FCC_HIGH_UA),
1905            sample_interval: None,
1906            ..Default::default()
1907        }]);
1908        h.expect_charger_watch().send(Err(fcharger::Error::AlreadyWatching)).unwrap();
1909        h.expect_reconnect();
1910    }
1911
1912    /// Verifies that closing the `Battery` channel ends the session and reconnects after
1913    /// `RECONNECT_DELAY`.
1914    #[fuchsia::test]
1915    fn test_loop_battery_channel_closed_reconnects() {
1916        let mut h = LoopHarness::new(vec![Evaluation {
1917            vote: fcc_vote(TEST_FCC_HIGH_UA),
1918            sample_interval: None,
1919            ..Default::default()
1920        }]);
1921        let watch = h.expect_battery_watch();
1922        drop(watch);
1923        h.battery = None;
1924        h.expect_reconnect();
1925    }
1926
1927    /// Verifies that closing the `Charger` channel reconnects all three protocols and re-sends the
1928    /// full vote and sample interval on the new session.
1929    #[fuchsia::test]
1930    fn test_loop_charger_channel_closed_reconnects_and_votes_again() {
1931        let mut h = LoopHarness::new(vec![Evaluation {
1932            vote: full_vote(),
1933            sample_interval: Some(TEST_SAMPLE_INTERVAL),
1934            ..Default::default()
1935        }]);
1936        let charger_watch = h.expect_charger_watch();
1937        let watch = h.expect_battery_watch();
1938        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1939        h.answer_no_charger();
1940        assert_eq!(h.accept_set_control(), full_vote());
1941        h.accept_sample_interval(Some(TEST_SAMPLE_INTERVAL));
1942        let _pending_watch = h.expect_battery_watch();
1943
1944        drop(charger_watch);
1945        h.charger = None;
1946        h.expect_reconnect();
1947
1948        // The new session starts over: the full vote and the interval are sent again.
1949        let _pending_charger_watch = h.expect_charger_watch();
1950        let watch = h.expect_battery_watch();
1951        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1952        h.answer_no_charger();
1953        assert_eq!(h.accept_set_control(), full_vote());
1954        h.accept_sample_interval(Some(TEST_SAMPLE_INTERVAL));
1955        let _pending_watch = h.expect_battery_watch();
1956        assert_eq!(h.evaluation_count(), 2);
1957    }
1958
1959    /// Verifies that a `SetControl` rejection keeps the current session alive and retries the
1960    /// rejected vote on the next telemetry update.
1961    #[fuchsia::test]
1962    fn test_loop_controller_rejection_keeps_session_and_retries() {
1963        let mut h = LoopHarness::new(vec![Evaluation {
1964            vote: fcc_vote(TEST_FCC_HIGH_UA),
1965            sample_interval: None,
1966            ..Default::default()
1967        }]);
1968        let _pending_charger_watch = h.expect_charger_watch();
1969        let watch = h.expect_battery_watch();
1970        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
1971        h.answer_no_charger();
1972        let (_, responder) = h.expect_set_control();
1973        responder.send(Err(fcharger::Error::InvalidArgs)).unwrap();
1974
1975        // The session is kept: Battery.Watch is re-armed and the next update retries the vote.
1976        let watch = h.expect_battery_watch();
1977        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
1978        assert_eq!(h.accept_set_control(), fcc_vote(TEST_FCC_HIGH_UA));
1979        let _pending_watch = h.expect_battery_watch();
1980        h.assert_no_controller_request();
1981        assert_eq!(h.evaluation_count(), 2);
1982        assert!(h.connected.borrow().is_empty(), "governor reconnected");
1983    }
1984
1985    /// Verifies that when both `Battery.Watch` and `Charger.Watch` are ready in the same turn,
1986    /// they are coalesced into a single evaluation without calling `Battery.GetStatus`.
1987    #[fuchsia::test]
1988    fn test_loop_battery_and_charger_ready_together_evaluate_once() {
1989        let mut h = LoopHarness::new(vec![Evaluation {
1990            vote: fcc_vote(TEST_FCC_HIGH_UA),
1991            sample_interval: None,
1992            ..Default::default()
1993        }]);
1994        let charger_watch = h.expect_charger_watch();
1995        let battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
1996        let online_charger = fcharger::Status { online: Some(true), ..Default::default() };
1997
1998        // As after a charger event that also changed the battery, battery-manager publishes the
1999        // battery status and then the charger status before the governor runs.
2000        battery_watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
2001        charger_watch.send(Ok((&online_charger, None))).unwrap();
2002
2003        // Both are evaluated together, without a Battery.GetStatus refresh, and both re-armed.
2004        let _pending_battery_watch = h.expect_battery_watch();
2005        let _pending_charger_watch = h.expect_charger_watch();
2006        h.assert_no_battery_request();
2007        h.assert_no_controller_request();
2008        assert_eq!(h.evaluation_count(), 2);
2009        assert_eq!(
2010            h.last_telemetry(),
2011            Telemetry {
2012                battery: battery_status(TEST_VBAT_UPDATED_UV),
2013                charger: Some(online_charger)
2014            }
2015        );
2016    }
2017
2018    /// Verifies that a `Battery.Watch` update evaluates with the charger status read via
2019    /// `Charger.GetStatus` rather than the last `Charger.Watch` response, as when the governor
2020    /// wakes between battery-manager's battery and charger responses to one charger event, and that
2021    /// the charger is read again once `Charger.Watch` reports after an earlier read found none.
2022    #[fuchsia::test]
2023    fn test_loop_battery_update_reads_current_charger_status() {
2024        let mut h = LoopHarness::new(vec![Evaluation {
2025            vote: fcc_vote(TEST_FCC_HIGH_UA),
2026            sample_interval: None,
2027            ..Default::default()
2028        }]);
2029        let charger_watch = h.expect_charger_watch();
2030        // No charger status yet: the read fails, and the next battery update does not retry it.
2031        let battery_watch = h.complete_initial_battery_update(TEST_VBAT_UV);
2032        battery_watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
2033        let battery_watch = h.expect_battery_watch();
2034        h.assert_no_charger_request();
2035
2036        // The charger reports, so later battery updates read it again.
2037        let offline = fcharger::Status { online: Some(false), ..Default::default() };
2038        charger_watch.send(Ok((&offline, None))).unwrap();
2039        h.expect_battery_get_status().send(Ok(&battery_status(TEST_VBAT_UV))).unwrap();
2040        let charger_watch = h.expect_charger_watch();
2041        assert_eq!(h.evaluation_count(), 3);
2042
2043        // battery-manager has published both statuses, but only the battery response has arrived.
2044        let online = fcharger::Status { online: Some(true), ..Default::default() };
2045        battery_watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
2046        h.expect_charger_get_status().send(Ok(&online)).unwrap();
2047        let _pending_battery_watch = h.expect_battery_watch();
2048        assert_eq!(h.evaluation_count(), 4);
2049        assert_eq!(
2050            h.last_telemetry(),
2051            Telemetry {
2052                battery: battery_status(TEST_VBAT_UPDATED_UV),
2053                charger: Some(online.clone())
2054            }
2055        );
2056
2057        // The late charger response evaluates the same snapshot again.
2058        charger_watch.send(Ok((&online, None))).unwrap();
2059        h.expect_battery_get_status().send(Ok(&battery_status(TEST_VBAT_UPDATED_UV))).unwrap();
2060        let _pending_charger_watch = h.expect_charger_watch();
2061        h.assert_no_controller_request();
2062        assert_eq!(h.evaluation_count(), 5);
2063        assert_eq!(
2064            h.last_telemetry(),
2065            Telemetry { battery: battery_status(TEST_VBAT_UPDATED_UV), charger: Some(online) }
2066        );
2067    }
2068
2069    /// Verifies that closing the `Controller` channel ends the session and reconnects after
2070    /// `RECONNECT_DELAY`.
2071    #[fuchsia::test]
2072    fn test_loop_controller_closed_reconnects() {
2073        let mut h = LoopHarness::new(vec![Evaluation {
2074            vote: fcc_vote(TEST_FCC_HIGH_UA),
2075            sample_interval: None,
2076            ..Default::default()
2077        }]);
2078        let _pending_charger_watch = h.expect_charger_watch();
2079        let watch = h.expect_battery_watch();
2080        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
2081        h.answer_no_charger();
2082        let (_, responder) = h.expect_set_control();
2083        drop(responder);
2084        h.controller = None;
2085        h.expect_reconnect();
2086    }
2087
2088    /// Verifies that when `Battery.ConfigureWatch` returns a clamped `max_interval` different from
2089    /// the requested `sample_interval`, the session remains healthy and caches the requested vote.
2090    #[fuchsia::test]
2091    fn test_loop_sample_interval_clamped_by_server() {
2092        let mut h = LoopHarness::new(vec![Evaluation {
2093            vote: fcc_vote(TEST_FCC_HIGH_UA),
2094            sample_interval: Some(TEST_SAMPLE_INTERVAL),
2095            ..Default::default()
2096        }]);
2097        let _pending_charger_watch = h.expect_charger_watch();
2098
2099        let watch = h.expect_battery_watch();
2100        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
2101        h.answer_no_charger();
2102        let _ = h.accept_set_control();
2103        let (_, responder) = h.expect_battery_configure_watch();
2104        responder
2105            .send(Ok(&fbattery::WatchOptions {
2106                max_interval: Some(TEST_LONGER_SAMPLE_INTERVAL.into_nanos()),
2107                ..Default::default()
2108            }))
2109            .unwrap();
2110
2111        let watch = h.expect_battery_watch();
2112        watch.send(Ok((&battery_status(TEST_VBAT_UPDATED_UV), None))).unwrap();
2113        let _pending_watch = h.expect_battery_watch();
2114        h.assert_no_battery_request();
2115        assert_eq!(h.evaluation_count(), 2);
2116    }
2117
2118    /// Verifies that a transport error on `Battery.ConfigureWatch` ends the session and reconnects.
2119    #[fuchsia::test]
2120    fn test_loop_sample_interval_transport_error_reconnects() {
2121        let mut h = LoopHarness::new(vec![Evaluation {
2122            vote: fcc_vote(TEST_FCC_HIGH_UA),
2123            sample_interval: Some(TEST_SAMPLE_INTERVAL),
2124            ..Default::default()
2125        }]);
2126        let _pending_charger_watch = h.expect_charger_watch();
2127
2128        let watch = h.expect_battery_watch();
2129        watch.send(Ok((&battery_status(TEST_VBAT_UV), None))).unwrap();
2130        h.answer_no_charger();
2131        let _ = h.accept_set_control();
2132        let (_, configure_responder) = h.expect_battery_configure_watch();
2133        drop(configure_responder);
2134        h.battery = None;
2135        h.expect_reconnect();
2136    }
2137
2138    /// Verifies that when `connect` returns `Err`, `supervise` waits `RECONNECT_DELAY` before
2139    /// retrying and starting the session.
2140    #[fuchsia::test]
2141    fn test_supervise_retries_failed_connect_after_delay() {
2142        let mut executor = fasync::TestExecutor::new_with_fake_time();
2143        let mut governor = SequenceGovernor::new(vec![Evaluation::default()]);
2144        let attempts = Rc::new(RefCell::new(0usize));
2145        let attempts_clone = attempts.clone();
2146        let connected: Rc<RefCell<Option<ServerEnds>>> = Rc::default();
2147        let connected_clone = connected.clone();
2148
2149        let mut run = Box::pin(async move {
2150            supervise(&mut governor, || {
2151                let mut count = attempts_clone.borrow_mut();
2152                *count += 1;
2153                if *count == 1 {
2154                    return Err(anyhow!("connect failed"));
2155                }
2156                let (session, server) = new_session();
2157                *connected_clone.borrow_mut() = Some(server);
2158                Ok(session)
2159            })
2160            .await
2161        });
2162
2163        assert!(executor.run_until_stalled(&mut run).is_pending());
2164        assert_eq!(*attempts.borrow(), 1);
2165        assert!(connected.borrow().is_none());
2166
2167        executor.set_fake_time(executor.now() + RECONNECT_DELAY - ONE_NANO);
2168        let _ = executor.wake_expired_timers();
2169        assert!(executor.run_until_stalled(&mut run).is_pending());
2170        assert_eq!(*attempts.borrow(), 1);
2171
2172        executor.set_fake_time(executor.now() + ONE_NANO);
2173        let _ = executor.wake_expired_timers();
2174        assert!(executor.run_until_stalled(&mut run).is_pending());
2175        assert_eq!(*attempts.borrow(), 2);
2176        assert!(connected.borrow().is_some());
2177    }
2178}