use core::time::Duration;
use assert_matches::assert_matches;
use net_types::ip::Ip;
use net_types::MulticastAddr;
use netstack3_base::Instant;
use packet_formats::utils::NonZeroDuration;
use rand::Rng;
use crate::internal::gmp::{
self, GmpBindingsContext, GmpContext, GmpContextInner, GmpGroupState, GmpMode, GmpStateRef,
GroupJoinResult, GroupLeaveResult, IpExt, NotAMemberErr, QueryTarget,
};
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub(super) struct DelayedReportTimerId<I: Ip>(pub(super) MulticastAddr<I::Addr>);
#[derive(Debug, Eq, PartialEq)]
pub(super) enum GmpMessageType {
Report,
Leave,
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) struct JoinGroupActions {
send_report_and_schedule_timer: Option<Duration>,
}
impl JoinGroupActions {
const NOOP: Self = Self { send_report_and_schedule_timer: None };
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) struct LeaveGroupActions {
send_leave: bool,
stop_timer: bool,
}
impl LeaveGroupActions {
const NOOP: Self = Self { send_leave: false, stop_timer: false };
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) struct ReportReceivedActions {
pub(super) stop_timer: bool,
}
impl ReportReceivedActions {
const NOOP: Self = Self { stop_timer: false };
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) enum QueryReceivedGenericAction {
ScheduleTimer(Duration),
StopTimerAndSendReport,
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) struct QueryReceivedActions<A> {
pub(super) generic: Option<QueryReceivedGenericAction>,
pub(super) protocol_specific: Option<A>,
}
impl<A> QueryReceivedActions<A> {
const NOOP: Self = Self { generic: None, protocol_specific: None };
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub(super) struct ReportTimerExpiredActions;
pub trait ProtocolConfig {
fn unsolicited_report_interval(&self) -> Duration;
fn send_leave_anyway(&self) -> bool;
fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration>;
type QuerySpecificActions;
fn do_query_received_specific(
&self,
max_resp_time: Duration,
) -> Option<Self::QuerySpecificActions>;
}
struct Transition<S, Actions>(S, Actions);
#[cfg_attr(test, derive(Debug))]
pub(super) struct NonMember;
#[cfg_attr(test, derive(Debug))]
pub(super) struct DelayingMember<I: Instant> {
timer_expiration: I,
last_reporter: bool,
}
#[cfg_attr(test, derive(Debug))]
pub(super) struct IdleMember {
last_reporter: bool,
}
#[cfg_attr(test, derive(Debug))]
pub(super) enum MemberState<I: Instant> {
NonMember(NonMember),
Delaying(DelayingMember<I>),
Idle(IdleMember),
}
impl<I: Instant> From<NonMember> for MemberState<I> {
fn from(s: NonMember) -> Self {
MemberState::NonMember(s)
}
}
impl<I: Instant> From<DelayingMember<I>> for MemberState<I> {
fn from(s: DelayingMember<I>) -> Self {
MemberState::Delaying(s)
}
}
impl<I: Instant> From<IdleMember> for MemberState<I> {
fn from(s: IdleMember) -> Self {
MemberState::Idle(s)
}
}
impl<S, A> Transition<S, A> {
fn into_state_actions<I: Instant>(self) -> (MemberState<I>, A)
where
MemberState<I>: From<S>,
{
(self.0.into(), self.1)
}
}
fn member_query_received<R: Rng, I: Instant, C: ProtocolConfig>(
rng: &mut R,
last_reporter: bool,
timer_expiration: Option<I>,
max_resp_time: Duration,
now: I,
cfg: &C,
) -> (MemberState<I>, QueryReceivedActions<C::QuerySpecificActions>) {
let ps_actions = cfg.do_query_received_specific(max_resp_time);
let (transition, generic_actions) = match cfg.get_max_resp_time(max_resp_time) {
None => (
IdleMember { last_reporter }.into(),
Some(QueryReceivedGenericAction::StopTimerAndSendReport),
),
Some(max_resp_time) => {
let max_resp_time = max_resp_time.get();
let new_deadline = now.saturating_add(max_resp_time);
let (timer_expiration, action) = match timer_expiration {
Some(old) if new_deadline >= old => (old, None),
None | Some(_) => {
let delay = gmp::random_report_timeout(rng, max_resp_time);
(
now.saturating_add(delay),
Some(QueryReceivedGenericAction::ScheduleTimer(delay)),
)
}
};
(DelayingMember { last_reporter, timer_expiration }.into(), action)
}
};
(transition, QueryReceivedActions { generic: generic_actions, protocol_specific: ps_actions })
}
impl NonMember {
fn join_group<I: Instant, R: Rng, C: ProtocolConfig>(
self,
rng: &mut R,
now: I,
cfg: &C,
) -> Transition<DelayingMember<I>, JoinGroupActions> {
let duration = cfg.unsolicited_report_interval();
let delay = gmp::random_report_timeout(rng, duration);
let actions = JoinGroupActions { send_report_and_schedule_timer: Some(delay) };
Transition(
DelayingMember {
last_reporter: true,
timer_expiration: now.checked_add(delay).expect("timer expiration overflowed"),
},
actions,
)
}
fn leave_group(self) -> Transition<NonMember, LeaveGroupActions> {
Transition(NonMember, LeaveGroupActions::NOOP)
}
}
impl<I: Instant> DelayingMember<I> {
fn query_received<R: Rng, C: ProtocolConfig>(
self,
rng: &mut R,
max_resp_time: Duration,
now: I,
cfg: &C,
) -> (MemberState<I>, QueryReceivedActions<C::QuerySpecificActions>) {
let DelayingMember { last_reporter, timer_expiration } = self;
member_query_received(rng, last_reporter, Some(timer_expiration), max_resp_time, now, cfg)
}
fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> Transition<NonMember, LeaveGroupActions> {
let actions = LeaveGroupActions {
send_leave: self.last_reporter || cfg.send_leave_anyway(),
stop_timer: true,
};
Transition(NonMember, actions)
}
fn report_received(self) -> Transition<IdleMember, ReportReceivedActions> {
Transition(IdleMember { last_reporter: false }, ReportReceivedActions { stop_timer: true })
}
fn report_timer_expired(self) -> Transition<IdleMember, ReportTimerExpiredActions> {
Transition(IdleMember { last_reporter: true }, ReportTimerExpiredActions)
}
}
impl IdleMember {
fn query_received<I: Instant, R: Rng, C: ProtocolConfig>(
self,
rng: &mut R,
max_resp_time: Duration,
now: I,
cfg: &C,
) -> (MemberState<I>, QueryReceivedActions<C::QuerySpecificActions>) {
let IdleMember { last_reporter } = self;
member_query_received(rng, last_reporter, None, max_resp_time, now, cfg)
}
fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> Transition<NonMember, LeaveGroupActions> {
let actions = LeaveGroupActions {
send_leave: self.last_reporter || cfg.send_leave_anyway(),
stop_timer: false,
};
Transition(NonMember, actions)
}
}
impl<I: Instant> MemberState<I> {
fn join_group<R: Rng, C: ProtocolConfig>(
cfg: &C,
rng: &mut R,
now: I,
gmp_disabled: bool,
) -> (MemberState<I>, JoinGroupActions) {
let non_member = NonMember;
if gmp_disabled {
(non_member.into(), JoinGroupActions::NOOP)
} else {
non_member.join_group(rng, now, cfg).into_state_actions()
}
}
fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> (MemberState<I>, LeaveGroupActions) {
match self {
MemberState::NonMember(state) => state.leave_group(),
MemberState::Delaying(state) => state.leave_group(cfg),
MemberState::Idle(state) => state.leave_group(cfg),
}
.into_state_actions()
}
fn query_received<R: Rng, C: ProtocolConfig>(
self,
rng: &mut R,
max_resp_time: Duration,
now: I,
cfg: &C,
) -> (MemberState<I>, QueryReceivedActions<C::QuerySpecificActions>) {
match self {
state @ MemberState::NonMember(_) => (state, QueryReceivedActions::NOOP),
MemberState::Delaying(state) => state.query_received(rng, max_resp_time, now, cfg),
MemberState::Idle(state) => state.query_received(rng, max_resp_time, now, cfg),
}
}
fn report_received(self) -> (MemberState<I>, ReportReceivedActions) {
match self {
state @ MemberState::Idle(_) | state @ MemberState::NonMember(_) => {
(state, ReportReceivedActions::NOOP)
}
MemberState::Delaying(state) => state.report_received().into_state_actions(),
}
}
fn report_timer_expired(self) -> (MemberState<I>, ReportTimerExpiredActions) {
match self {
MemberState::Idle(_) | MemberState::NonMember(_) => {
unreachable!("got report timer in non-delaying state")
}
MemberState::Delaying(state) => state.report_timer_expired().into_state_actions(),
}
}
}
#[cfg_attr(test, derive(Debug))]
pub struct GmpStateMachine<I: Instant> {
pub(super) inner: Option<MemberState<I>>,
}
impl<I: Instant> GmpStateMachine<I> {
pub(super) fn join_group<R: Rng, C: ProtocolConfig>(
rng: &mut R,
now: I,
gmp_disabled: bool,
cfg: &C,
) -> (GmpStateMachine<I>, JoinGroupActions) {
let (state, actions) = MemberState::join_group(cfg, rng, now, gmp_disabled);
(GmpStateMachine { inner: Some(state) }, actions)
}
fn join_if_non_member<R: Rng, C: ProtocolConfig>(
&mut self,
rng: &mut R,
now: I,
cfg: &C,
) -> JoinGroupActions {
self.update(|s| match s {
MemberState::NonMember(s) => s.join_group(rng, now, cfg).into_state_actions(),
state @ MemberState::Delaying(_) | state @ MemberState::Idle(_) => {
(state, JoinGroupActions::NOOP)
}
})
}
fn leave_if_member<C: ProtocolConfig>(&mut self, cfg: &C) -> LeaveGroupActions {
self.update(|s| s.leave_group(cfg))
}
pub(super) fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> LeaveGroupActions {
let (_state, actions) = self.inner.unwrap().leave_group(cfg);
actions
}
pub(super) fn query_received<R: Rng, C: ProtocolConfig>(
&mut self,
rng: &mut R,
max_resp_time: Duration,
now: I,
cfg: &C,
) -> QueryReceivedActions<C::QuerySpecificActions> {
self.update(|s| s.query_received(rng, max_resp_time, now, cfg))
}
pub(super) fn report_received(&mut self) -> ReportReceivedActions {
self.update(MemberState::report_received)
}
pub(super) fn report_timer_expired(&mut self) -> ReportTimerExpiredActions {
self.update(MemberState::report_timer_expired)
}
fn update<A, F: FnOnce(MemberState<I>) -> (MemberState<I>, A)>(&mut self, f: F) -> A {
let (s, a) = f(self.inner.take().unwrap());
self.inner = Some(s);
a
}
pub(super) fn new_for_mode_transition() -> Self {
Self { inner: Some(MemberState::Idle(IdleMember { last_reporter: false })) }
}
#[cfg(test)]
pub(super) fn get_inner(&self) -> &MemberState<I> {
self.inner.as_ref().unwrap()
}
}
pub(super) fn handle_timer<I, CC, BC>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
state: GmpStateRef<'_, I, CC, BC>,
timer: DelayedReportTimerId<I>,
) where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
debug_assert!(gmp.mode.is_v1());
let DelayedReportTimerId(group_addr) = timer;
let ReportTimerExpiredActions {} = groups
.get_mut(&group_addr)
.expect("get state for group with expired report timer")
.v1_mut()
.report_timer_expired();
core_ctx.send_message_v1(bindings_ctx, &device, group_addr, GmpMessageType::Report);
}
pub(super) fn handle_report_message<I, BC, CC>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
group_addr: MulticastAddr<I::Addr>,
) -> Result<(), NotAMemberErr<I>>
where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
core_ctx.with_gmp_state_mut(device, |state| {
let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
if !gmp.mode.is_v1() {
return Ok(());
}
let ReportReceivedActions { stop_timer } = groups
.get_mut(&group_addr)
.ok_or_else(|| NotAMemberErr(*group_addr))
.map(|a| a.v1_mut().report_received())?;
if stop_timer {
assert_matches!(
gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into()),
Some(_)
);
}
Ok(())
})
}
pub(super) trait QueryMessage<I: Ip> {
fn group_addr(&self) -> I::Addr;
fn max_response_time(&self) -> Duration;
}
pub(super) fn handle_query_message<I, CC, BC, Q>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
query: &Q,
) -> Result<(), NotAMemberErr<I>>
where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
Q: QueryMessage<I>,
{
core_ctx.with_gmp_state_mut_and_ctx(device, |mut core_ctx, mut state| {
gmp::enter_mode(
&mut core_ctx,
bindings_ctx,
device,
state.as_mut(),
GmpMode::V1 { compat: true },
);
if state.gmp.mode.is_v1_compat() {
gmp::schedule_v1_compat(bindings_ctx, state.as_mut())
}
handle_query_message_inner(&mut core_ctx, bindings_ctx, device, state, query)
})
}
pub(super) fn handle_query_message_inner<I, CC, BC, Q>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
state: GmpStateRef<'_, I, CC, BC>,
query: &Q,
) -> Result<(), NotAMemberErr<I>>
where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
Q: QueryMessage<I>,
{
let GmpStateRef { enabled: _, groups, gmp, config } = state;
let now = bindings_ctx.now();
let target = query.group_addr();
let target = QueryTarget::new(target).ok_or_else(|| NotAMemberErr(target))?;
let iter = match target {
QueryTarget::Unspecified => {
either::Either::Left(groups.iter_mut().map(|(addr, state)| (*addr, state)))
}
QueryTarget::Specified(group_addr) => either::Either::Right(core::iter::once((
group_addr,
groups.get_mut(&group_addr).ok_or_else(|| NotAMemberErr(*group_addr))?,
))),
};
for (group_addr, state) in iter {
let QueryReceivedActions { generic, protocol_specific } = state.v1_mut().query_received(
&mut bindings_ctx.rng(),
query.max_response_time(),
now,
config,
);
let send_msg = generic.and_then(|generic| match generic {
QueryReceivedGenericAction::ScheduleTimer(delay) => {
let _: Option<(BC::Instant, ())> = gmp.timers.schedule_after(
bindings_ctx,
DelayedReportTimerId(group_addr).into(),
(),
delay,
);
None
}
QueryReceivedGenericAction::StopTimerAndSendReport => {
let _: Option<(BC::Instant, ())> =
gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into());
Some(GmpMessageType::Report)
}
});
if let Some(ps_actions) = protocol_specific {
core_ctx.run_actions(bindings_ctx, device, ps_actions);
}
if let Some(msg) = send_msg {
core_ctx.send_message_v1(bindings_ctx, device, group_addr, msg);
}
}
Ok(())
}
pub(super) fn handle_enabled<I, CC, BC>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
state: GmpStateRef<'_, I, CC, BC>,
) where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
let GmpStateRef { enabled: _, groups, gmp, config } = state;
debug_assert!(gmp.mode.is_v1());
let now = bindings_ctx.now();
for (group_addr, state) in groups.iter_mut() {
let group_addr = *group_addr;
if !I::should_perform_gmp(group_addr) {
continue;
}
let JoinGroupActions { send_report_and_schedule_timer } =
state.v1_mut().join_if_non_member(&mut bindings_ctx.rng(), now, config);
let Some(delay) = send_report_and_schedule_timer else {
continue;
};
assert_matches!(
gmp.timers.schedule_after(
bindings_ctx,
DelayedReportTimerId(group_addr).into(),
(),
delay
),
None
);
core_ctx.send_message_v1(bindings_ctx, device, group_addr, GmpMessageType::Report);
}
}
pub(super) fn handle_disabled<I, CC, BC>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
state: GmpStateRef<'_, I, CC, BC>,
) where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
let GmpStateRef { enabled: _, groups, gmp, config } = state;
debug_assert!(gmp.mode.is_v1());
for (group_addr, state) in groups.groups_mut() {
let LeaveGroupActions { send_leave, stop_timer } = state.v1_mut().leave_if_member(config);
if stop_timer {
assert_matches!(
gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(*group_addr).into()),
Some(_)
);
}
if send_leave {
core_ctx.send_message_v1(bindings_ctx, device, *group_addr, GmpMessageType::Leave);
}
}
}
pub(super) fn join_group<I, CC, BC>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
group_addr: MulticastAddr<I::Addr>,
state: GmpStateRef<'_, I, CC, BC>,
) -> GroupJoinResult
where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
let GmpStateRef { enabled, groups, gmp, config } = state;
debug_assert!(gmp.mode.is_v1());
let now = bindings_ctx.now();
let gmp_disabled = !enabled || !I::should_perform_gmp(group_addr);
let result = groups.join_group_with(group_addr, || {
let (state, actions) =
GmpStateMachine::join_group(&mut bindings_ctx.rng(), now, gmp_disabled, config);
(GmpGroupState::new_v1(state), actions)
});
result.map(|JoinGroupActions { send_report_and_schedule_timer }| {
if let Some(delay) = send_report_and_schedule_timer {
assert_matches!(
gmp.timers.schedule_after(
bindings_ctx,
DelayedReportTimerId(group_addr).into(),
(),
delay
),
None
);
core_ctx.send_message_v1(bindings_ctx, device, group_addr, GmpMessageType::Report);
}
})
}
pub(super) fn leave_group<I, CC, BC>(
core_ctx: &mut CC::Inner<'_>,
bindings_ctx: &mut BC,
device: &CC::DeviceId,
group_addr: MulticastAddr<I::Addr>,
state: GmpStateRef<'_, I, CC, BC>,
) -> GroupLeaveResult
where
BC: GmpBindingsContext,
CC: GmpContext<I, BC>,
I: IpExt,
{
let GmpStateRef { enabled: _, groups, gmp, config } = state;
debug_assert!(gmp.mode.is_v1());
groups.leave_group(group_addr).map(|state| {
let LeaveGroupActions { send_leave, stop_timer } = state.into_v1().leave_group(config);
if stop_timer {
assert_matches!(
gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into()),
Some(_)
);
}
if send_leave {
core_ctx.send_message_v1(bindings_ctx, device, group_addr, GmpMessageType::Leave);
}
})
}
#[cfg(test)]
mod test {
use core::convert::Infallible as Never;
use assert_matches::assert_matches;
use ip_test_macro::ip_test;
use netstack3_base::testutil::{new_rng, FakeDeviceId, FakeInstant};
use test_util::assert_lt;
use super::*;
const DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
#[derive(Debug, Default)]
struct FakeConfig(bool);
impl ProtocolConfig for FakeConfig {
fn unsolicited_report_interval(&self) -> Duration {
DEFAULT_UNSOLICITED_REPORT_INTERVAL
}
fn send_leave_anyway(&self) -> bool {
let Self(cfg) = self;
*cfg
}
fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration> {
NonZeroDuration::new(resp_time)
}
type QuerySpecificActions = Never;
fn do_query_received_specific(&self, _max_resp_time: Duration) -> Option<Never> {
None
}
}
type FakeGmpStateMachine = GmpStateMachine<FakeInstant>;
#[test]
fn test_gmp_state_non_member_to_delay_should_set_flag() {
let (s, _actions) = FakeGmpStateMachine::join_group(
&mut new_rng(0),
FakeInstant::default(),
false,
&FakeConfig::default(),
);
match s.get_inner() {
MemberState::Delaying(s) => assert!(s.last_reporter),
_ => panic!("Wrong State!"),
}
}
#[test]
fn test_gmp_state_non_member_to_delay_actions() {
let (_state, actions) = FakeGmpStateMachine::join_group(
&mut new_rng(0),
FakeInstant::default(),
false,
&FakeConfig::default(),
);
assert_matches!(
actions,
JoinGroupActions { send_report_and_schedule_timer: Some(d) } if d <= DEFAULT_UNSOLICITED_REPORT_INTERVAL
);
}
#[test]
fn test_gmp_state_delay_no_reset_timer() {
let mut rng = new_rng(0);
let cfg = FakeConfig::default();
let (mut s, _actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(
s.query_received(
&mut rng,
DEFAULT_UNSOLICITED_REPORT_INTERVAL + Duration::from_secs(1),
FakeInstant::default(),
&cfg
),
QueryReceivedActions { generic: None, protocol_specific: None }
);
}
#[test]
fn test_gmp_state_delay_reset_timer() {
let mut rng = new_rng(10);
let cfg = FakeConfig::default();
let (mut s, JoinGroupActions { send_report_and_schedule_timer }) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
let first_duration = send_report_and_schedule_timer.expect("starts delaying member");
let actions = s.query_received(
&mut rng,
first_duration.checked_sub(Duration::from_micros(1)).unwrap(),
FakeInstant::default(),
&cfg,
);
let new_duration = assert_matches!(actions,
QueryReceivedActions {
generic: Some(QueryReceivedGenericAction::ScheduleTimer(d)),
protocol_specific: None
} => d
);
assert_lt!(new_duration, first_duration);
}
#[test]
fn test_gmp_state_delay_to_idle_with_report_no_flag() {
let (mut s, _actions) = FakeGmpStateMachine::join_group(
&mut new_rng(0),
FakeInstant::default(),
false,
&FakeConfig::default(),
);
assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
match s.get_inner() {
MemberState::Idle(s) => {
assert!(!s.last_reporter);
}
_ => panic!("Wrong State!"),
}
}
#[test]
fn test_gmp_state_delay_to_idle_without_report_set_flag() {
let (mut s, _actions) = FakeGmpStateMachine::join_group(
&mut new_rng(0),
FakeInstant::default(),
false,
&FakeConfig::default(),
);
assert_eq!(s.report_timer_expired(), ReportTimerExpiredActions,);
match s.get_inner() {
MemberState::Idle(s) => {
assert!(s.last_reporter);
}
_ => panic!("Wrong State!"),
}
}
#[test]
fn test_gmp_state_leave_should_send_leave() {
let mut rng = new_rng(0);
let cfg = FakeConfig::default();
let (s, _actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: true });
let (mut s, _actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(s.report_timer_expired(), ReportTimerExpiredActions,);
assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: false });
}
#[test]
fn test_gmp_state_delay_to_other_states_should_stop_timer() {
let mut rng = new_rng(0);
let cfg = FakeConfig::default();
let (s, _actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: true },);
let (mut s, _actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
}
#[test]
fn test_gmp_state_other_states_to_delay_should_schedule_timer() {
let mut rng = new_rng(0);
let cfg = FakeConfig::default();
let (mut s, actions) =
FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
assert_matches!(
actions,
JoinGroupActions { send_report_and_schedule_timer: Some(d) } if d <= DEFAULT_UNSOLICITED_REPORT_INTERVAL
);
assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
assert_eq!(
s.query_received(&mut rng, Duration::from_secs(1), FakeInstant::default(), &cfg),
QueryReceivedActions {
generic: Some(QueryReceivedGenericAction::ScheduleTimer(Duration::from_micros(1))),
protocol_specific: None
}
);
}
#[test]
fn test_gmp_state_leave_send_anyway_do_send() {
let mut cfg = FakeConfig::default();
let (mut s, _actions) =
FakeGmpStateMachine::join_group(&mut new_rng(0), FakeInstant::default(), false, &cfg);
cfg = FakeConfig(true);
assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
match s.get_inner() {
MemberState::Idle(s) => assert!(!s.last_reporter),
_ => panic!("Wrong State!"),
}
assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: false });
}
#[test]
fn test_gmp_state_leave_not_the_last_do_nothing() {
let cfg = FakeConfig::default();
let (mut s, _actions) =
FakeGmpStateMachine::join_group(&mut new_rng(0), FakeInstant::default(), false, &cfg);
assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: false, stop_timer: false })
}
#[ip_test(I)]
fn ignores_reports_if_v2<I: gmp::testutil::TestIpExt>() {
let gmp::testutil::FakeCtx { mut core_ctx, mut bindings_ctx } =
gmp::testutil::new_context_with_mode::<I>(GmpMode::V2);
assert_eq!(
handle_report_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
Ok(())
);
core_ctx.gmp.timers.assert_timers([]);
assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
}
}