1use crate::{NetworkMessage, fuchsia_network_monitor_fs};
6use bstr::BString;
7use fuchsia_component::client::connect_to_protocol_sync;
8use fuchsia_inspect_derive::{IValue, Inspect, Unit, WithInspect};
9use starnix_core::task::Kernel;
10use starnix_core::vfs::fs_registry::FsRegistry;
11use starnix_logging::{log_error, log_info};
12use starnix_sync::{LockDepGuard, LockDepMutex, NmfsNetworkManagerLock};
13use starnix_uapi::error;
14use starnix_uapi::errors::Errno;
15use starnix_uapi::fs_type::FileSystemTypeFlags;
16use std::collections::HashMap;
17use std::collections::hash_map::Entry;
18use thiserror::Error;
19
20use fidl_fuchsia_net_policy_properties as fnp_properties;
21
22#[derive(Inspect)]
24pub(crate) struct NetworkManager {
25 network_registry: fnp_properties::NetworkRegistrySynchronousProxy,
26 #[inspect(forward)]
27 inner: LockDepMutex<IValue<NetworkManagerInner>, NmfsNetworkManagerLock>,
28}
29
30#[derive(Unit, Default)]
31struct NetworkManagerInner {
32 #[inspect(skip)]
34 default_id: Option<u32>,
35 #[inspect(skip)]
36 networks: HashMap<u32, Option<NetworkMessage>>,
37
38 default_ids_set: SeenSentData,
39 added_networks: SeenSentData,
40 updated_networks: SeenSentData,
41 removed_networks: SeenSentData,
42}
43
44#[derive(Unit, Default)]
45struct SeenSentData {
46 seen: u64,
49 sent: u64,
52}
53
54pub fn nmfs_init(kernel: &Kernel) -> Result<(), anyhow::Error> {
56 let registry = kernel.expando.get::<FsRegistry>();
58 registry.register(
59 b"fuchsia_network_monitor_fs".into(),
60 FileSystemTypeFlags::empty(),
61 fuchsia_network_monitor_fs,
62 );
63
64 let network_registry = connect_to_protocol_sync::<fnp_properties::NetworkRegistryMarker>()?;
66 kernel
67 .expando
68 .get_or_init(|| NetworkManager::new_with_proxy(network_registry, &kernel.inspect_node));
69 Ok(())
70}
71
72impl NetworkManager {
76 pub(crate) fn new_with_proxy(
78 proxy: fnp_properties::NetworkRegistrySynchronousProxy,
79 node: &fuchsia_inspect::Node,
80 ) -> Self {
81 Self { network_registry: proxy, inner: Default::default() }
82 .with_inspect(node, "nmfs")
83 .expect("Failed to attach 'nmfs' node")
84 }
85
86 fn lock(&self) -> LockDepGuard<'_, IValue<NetworkManagerInner>> {
88 self.inner.lock()
89 }
90
91 pub(crate) fn get_default_network_id(&self) -> Option<u32> {
92 self.lock().default_id
93 }
94
95 pub(crate) fn get_network(&self, network_id: &u32) -> Option<Option<NetworkMessage>> {
96 self.lock().networks.get(network_id).cloned()
97 }
98
99 pub(crate) fn get_default_id_as_bytes(&self) -> BString {
100 let default_id = match self.get_default_network_id() {
101 Some(id) => id.to_string(),
102 None => "".to_string(),
103 };
104 default_id.into_bytes().into()
105 }
106
107 pub(crate) fn get_network_by_id_as_bytes(&self, network_id: u32) -> BString {
108 let network_info = match self.get_network(&network_id) {
109 Some(Some(network)) => {
110 serde_json::to_string(&network).unwrap_or_else(|_| "{}".to_string())
111 }
112 Some(None) | None => "{}".to_string(),
115 };
116 network_info.into_bytes().into()
117 }
118
119 pub(crate) fn set_default_network_id(&self, network_id: Option<u32>) {
122 {
123 let mut inner_guard = self.lock();
124 let mut inner = inner_guard.as_mut();
125 inner.default_id = network_id;
126 inner.default_ids_set.seen += 1;
127 }
128
129 match self.fidl_set_default_network_id(network_id) {
131 Ok(()) => {
132 log_info!("Successfully set network with id {network_id:?} as default in netcfg",);
133 let mut inner_guard = self.lock();
134 inner_guard.as_mut().default_ids_set.sent += 1;
135 }
136 Err(e) => {
137 log_error!(
138 "Failed to set network with id {network_id:?} as default in netcfg; {e:?}"
139 );
140 }
141 }
142 }
143
144 pub(crate) fn add_empty_network(&self, network_id: u32) -> Result<(), Errno> {
150 let mut inner_guard = self.lock();
151 match inner_guard.as_mut().networks.entry(network_id) {
152 Entry::Occupied(_) => {
153 log_error!(
154 "Failed to add empty network to HashMap, was present for id: {}",
155 network_id
156 );
157 return error!(EEXIST);
158 }
159 Entry::Vacant(entry) => entry.insert(None),
160 };
161 Ok(())
162 }
163
164 pub(crate) fn add_network(&self, network: NetworkMessage) -> Result<(), Errno> {
169 {
170 let mut inner_guard = self.lock();
171 let mut inner = inner_guard.as_mut();
172 match inner.networks.entry(network.netid) {
173 Entry::Occupied(mut entry) => {
174 if let Some(network) = entry.get() {
177 log_error!(
178 "Failed to add network with id {} to HashMap, already existed",
179 network.netid
180 );
181 return error!(EEXIST);
182 }
183 let _ = entry.insert(Some(network.clone()));
184 }
185 Entry::Vacant(entry) => {
186 let _ = entry.insert(Some(network.clone()));
187 }
188 }
189 inner.added_networks.seen += 1;
190 }
191
192 match self.fidl_add_network(&fnp_properties::Network::from(&network)) {
194 Ok(()) => {
195 log_info!("Successfully added network with id {} to netcfg", network.netid);
196 let mut inner_guard = self.lock();
197 inner_guard.as_mut().added_networks.sent += 1;
198 }
199 Err(e) => {
200 log_error!("Failed to add network with id {:?} to netcfg; {e:?}", network.netid);
201 }
202 }
203
204 Ok(())
205 }
206
207 pub(crate) fn update_network(&self, network: NetworkMessage) -> Result<(), Errno> {
212 {
213 let mut inner_guard = self.lock();
214 let mut inner = inner_guard.as_mut();
215 let _old_network = match inner.networks.entry(network.netid) {
218 Entry::Occupied(mut entry) => {
219 if let None = entry.get() {
220 return error!(ENOENT);
221 }
222 entry.insert(Some(network.clone()))
223 }
224 Entry::Vacant(_) => {
225 return error!(ENOENT);
226 }
227 };
228 inner.updated_networks.seen += 1;
229 };
230
231 match self.fidl_update_network(&fnp_properties::Network::from(&network)) {
233 Ok(()) => {
234 log_info!("Successfully updated network with id {} in netcfg", network.netid);
235 let mut inner_guard = self.lock();
236 inner_guard.as_mut().updated_networks.sent += 1;
237 }
238 Err(e) => {
239 log_error!("Failed to update network with id {} in netcfg; {e:?}", network.netid);
240 }
241 }
242
243 Ok(())
244 }
245
246 pub(crate) fn remove_network(&self, network_id: u32) -> Result<(), Errno> {
251 let default_network_id = self.get_default_network_id();
254 if let Some(id) = default_network_id {
255 if id == network_id {
256 return error!(EPERM);
257 }
258 }
259 {
260 let mut inner_guard = self.lock();
261 let mut inner = inner_guard.as_mut();
262 if let None = inner.networks.remove(&network_id) {
263 return error!(ENOENT);
264 }
265 inner.removed_networks.seen += 1;
266 }
267
268 match self.fidl_remove_network(&network_id) {
270 Ok(()) => {
271 log_info!("Successfully removed network with id {network_id} from netcfg",);
272 let mut inner_guard = self.lock();
273 inner_guard.as_mut().removed_networks.sent += 1;
274 }
275 Err(e) => {
276 log_error!("Failed to remove network with id {network_id} in netcfg; {e:?}");
277 }
278 }
279 Ok(())
280 }
281
282 fn fidl_set_default_network_id(
284 &self,
285 network_id: Option<u32>,
286 ) -> Result<(), NetworkManagerError> {
287 let network_id = match network_id {
288 Some(id) => fidl_fuchsia_posix_socket::OptionalUint32::Value(id),
289 None => {
290 fidl_fuchsia_posix_socket::OptionalUint32::Unset(fidl_fuchsia_posix_socket::Empty)
291 }
292 };
293 Ok(self.network_registry.set_default(&network_id, zx::MonotonicInstant::INFINITE)??)
294 }
295
296 fn fidl_add_network(
298 &self,
299 network: &fnp_properties::Network,
300 ) -> Result<(), NetworkManagerError> {
301 Ok(self.network_registry.add(&network, zx::MonotonicInstant::INFINITE)??)
302 }
303
304 fn fidl_update_network(
306 &self,
307 network: &fnp_properties::Network,
308 ) -> Result<(), NetworkManagerError> {
309 Ok(self.network_registry.update(&network, zx::MonotonicInstant::INFINITE)??)
310 }
311
312 fn fidl_remove_network(&self, network_id: &u32) -> Result<(), NetworkManagerError> {
314 Ok(self.network_registry.remove(*network_id, zx::MonotonicInstant::INFINITE)??)
315 }
316}
317
318#[derive(Clone, Debug, Error)]
321pub(crate) enum NetworkManagerError {
322 #[error("Error during netcfg Add: {0:?}")]
323 Add(fnp_properties::NetworkRegistryAddError),
324 #[error("Error calling FIDL on netcfg: {0:?}")]
325 Fidl(#[from] fidl::Error),
326 #[error("Error during netcfg Remove: {0:?}")]
327 Remove(fnp_properties::NetworkRegistryRemoveError),
328 #[error("Error during netcfg SetDefault: {0:?}")]
329 SetDefault(fnp_properties::NetworkRegistrySetDefaultError),
330 #[error("Error during netcfg Update: {0:?}")]
331 Update(fnp_properties::NetworkRegistryUpdateError),
332}
333
334impl From<fnp_properties::NetworkRegistryAddError> for NetworkManagerError {
335 fn from(error: fnp_properties::NetworkRegistryAddError) -> Self {
336 NetworkManagerError::Add(error)
337 }
338}
339
340impl From<fnp_properties::NetworkRegistryRemoveError> for NetworkManagerError {
341 fn from(error: fnp_properties::NetworkRegistryRemoveError) -> Self {
342 NetworkManagerError::Remove(error)
343 }
344}
345
346impl From<fnp_properties::NetworkRegistrySetDefaultError> for NetworkManagerError {
347 fn from(error: fnp_properties::NetworkRegistrySetDefaultError) -> Self {
348 NetworkManagerError::SetDefault(error)
349 }
350}
351
352impl From<fnp_properties::NetworkRegistryUpdateError> for NetworkManagerError {
353 fn from(error: fnp_properties::NetworkRegistryUpdateError) -> Self {
354 NetworkManagerError::Update(error)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 use assert_matches::assert_matches;
363 use diagnostics_assertions::assert_data_tree;
364 use futures::StreamExt as _;
365 use starnix_uapi::{EEXIST, ENOENT};
366 use test_case::test_case;
367
368 fn test_network_message_from_id(netid: u32) -> NetworkMessage {
369 NetworkMessage { netid, ..Default::default() }
370 }
371
372 #[::fuchsia::test]
373 async fn test_add_empty_network() {
374 let inspector = fuchsia_inspect::Inspector::default();
375 let manager = &setup_proxy(inspector.root(), vec![]);
376
377 let network_id = 1;
378 assert_matches!(manager.add_empty_network(network_id), Ok(()));
379
380 assert_matches!(
382 manager.add_empty_network(network_id),
383 Err(errno) if errno.code.error_code() == EEXIST
384 );
385 assert_matches!(manager.get_network(&network_id), Some(None));
386 assert_data_tree!(inspector, root: {
389 nmfs: contains {
390 added_networks: {
391 seen: 0u64,
392 sent: 0u64,
393 },
394 },
395 });
396 }
397
398 fn setup_proxy(
401 inspect_node: &fuchsia_inspect::Node,
402 results: Vec<Result<(), NetworkManagerError>>,
403 ) -> NetworkManager {
404 let (proxy, mut stream) = fidl::endpoints::create_sync_proxy_and_stream::<
405 fnp_properties::NetworkRegistryMarker,
406 >();
407 let manager = NetworkManager::new_with_proxy(proxy, inspect_node);
408
409 let mut results = results.into_iter();
410 fuchsia_async::Task::spawn(async move {
411 while let Some(item) = stream.next().await {
412 let result = results
413 .next()
414 .expect("there should be an equivalent # of results and requests");
415 match item.expect("receive request") {
416 fnp_properties::NetworkRegistryRequest::SetDefault {
417 network_id: _,
418 responder,
419 } => {
420 let res = result.map_err(|e| match e {
421 NetworkManagerError::SetDefault(err) => err,
422 _ => unreachable!("should have been SetDefault error variant"),
423 });
424 responder.send(res).expect("respond to SetDefault");
425 }
426 fnp_properties::NetworkRegistryRequest::Add { network: _, responder } => {
427 let res = result.map_err(|e| match e {
428 NetworkManagerError::Add(err) => err,
429 _ => unreachable!("should have been Add error variant"),
430 });
431 responder.send(res).expect("respond to Add");
432 }
433 fnp_properties::NetworkRegistryRequest::Update { network: _, responder } => {
434 let res = result.map_err(|e| match e {
435 NetworkManagerError::Update(err) => err,
436 _ => unreachable!("should have been Update error variant"),
437 });
438 responder.send(res).expect("respond to Update");
439 }
440 fnp_properties::NetworkRegistryRequest::Remove { network_id: _, responder } => {
441 let res = result.map_err(|e| match e {
442 NetworkManagerError::Remove(err) => err,
443 _ => unreachable!("should have been Remove error variant"),
444 });
445 responder.send(res).expect("respond to Remove");
446 }
447 }
448 }
449 })
450 .detach();
451 manager
452 }
453
454 struct SeenSentData {
457 seen: u64,
458 sent: u64,
459 }
460
461 #[test_case(vec![Ok(()), Ok(())], SeenSentData { seen: 2, sent: 2 }; "all_success")]
462 #[test_case(
463 vec![
464 Ok(()),
465 Err(NetworkManagerError::SetDefault(fnp_properties::NetworkRegistrySetDefaultError::NotFound)),
466 ],
467 SeenSentData { seen: 2, sent: 1 };
468 "one_error"
469 )]
470 #[test_case(
471 vec![
472 Err(NetworkManagerError::SetDefault(fnp_properties::NetworkRegistrySetDefaultError::NotFound)),
473 Err(NetworkManagerError::SetDefault(fnp_properties::NetworkRegistrySetDefaultError::NotFound)),
474 ],
475 SeenSentData { seen: 2, sent: 0 };
476 "all_errors"
477 )]
478 #[::fuchsia::test(threads = 2)]
479 async fn test_set_default_network_id_with_proxy(
480 results: Vec<Result<(), NetworkManagerError>>,
481 expected_data: SeenSentData,
482 ) {
483 let inspector = fuchsia_inspect::Inspector::default();
484 let manager = &setup_proxy(inspector.root(), results);
485
486 manager.set_default_network_id(Some(1));
487 assert_eq!(manager.get_default_network_id(), Some(1));
488
489 manager.set_default_network_id(Some(2));
490 assert_eq!(manager.get_default_network_id(), Some(2));
491
492 assert_data_tree!(inspector, root: {
493 nmfs: contains {
494 default_ids_set: {
495 seen: expected_data.seen,
496 sent: expected_data.sent,
497 },
498 added_networks: {
499 seen: 0u64,
500 sent: 0u64,
501 },
502 updated_networks: {
503 seen: 0u64,
504 sent: 0u64,
505 },
506 removed_networks: {
507 seen: 0u64,
508 sent: 0u64,
509 },
510 },
511 });
512 }
513
514 #[test_case(vec![Ok(()), Ok(())], SeenSentData { seen: 2, sent: 2 }; "all_success")]
515 #[test_case(
516 vec![
517 Ok(()),
518 Err(NetworkManagerError::Add(fnp_properties::NetworkRegistryAddError::DuplicateNetworkId)),
519 ],
520 SeenSentData { seen: 2, sent: 1 };
521 "one_error"
522 )]
523 #[test_case(
524 vec![
525 Err(NetworkManagerError::Add(fnp_properties::NetworkRegistryAddError::MissingNetworkId)),
526 Err(NetworkManagerError::Add(fnp_properties::NetworkRegistryAddError::MissingNetworkInfo)),
527 ],
528 SeenSentData { seen: 2, sent: 0 };
529 "all_errors"
530 )]
531 #[::fuchsia::test(threads = 2)]
532 async fn test_add_network_with_proxy(
533 results: Vec<Result<(), NetworkManagerError>>,
534 expected_data: SeenSentData,
535 ) {
536 let inspector = fuchsia_inspect::Inspector::default();
537 let manager = &setup_proxy(inspector.root(), results);
538
539 let network1 = test_network_message_from_id(1);
543 assert_matches!(manager.add_network(network1.clone()), Ok(()));
544
545 let network2 = test_network_message_from_id(2);
546 assert_matches!(manager.add_network(network2.clone()), Ok(()));
547
548 assert_matches!(
551 manager.add_network(network2.clone()),
552 Err(errno) if errno.code.error_code() == EEXIST
553 );
554
555 assert_data_tree!(inspector, root: {
556 nmfs: contains {
557 default_ids_set: {
558 seen: 0u64,
559 sent: 0u64,
560 },
561 added_networks: {
562 seen: expected_data.seen,
563 sent: expected_data.sent,
564 },
565 updated_networks: {
566 seen: 0u64,
567 sent: 0u64,
568 },
569 removed_networks: {
570 seen: 0u64,
571 sent: 0u64,
572 },
573 },
574 });
575 }
576
577 #[test_case(vec![Ok(()), Ok(())], SeenSentData { seen: 2, sent: 2 }; "all_success")]
578 #[test_case(
579 vec![
580 Ok(()),
581 Err(NetworkManagerError::Update(fnp_properties::NetworkRegistryUpdateError::MissingNetworkId)),
582 ],
583 SeenSentData { seen: 2, sent: 1 };
584 "one_error"
585 )]
586 #[test_case(
587 vec![
588 Err(NetworkManagerError::Update(fnp_properties::NetworkRegistryUpdateError::NotFound)),
589 Err(NetworkManagerError::Update(fnp_properties::NetworkRegistryUpdateError::MissingNetworkInfo)),
590 ],
591 SeenSentData { seen: 2, sent: 0 };
592 "all_errors"
593 )]
594 #[::fuchsia::test(threads = 2)]
595 async fn test_update_network_with_proxy(
596 results: Vec<Result<(), NetworkManagerError>>,
597 expected_data: SeenSentData,
598 ) {
599 let inspector = fuchsia_inspect::Inspector::default();
600 let manager = &setup_proxy(inspector.root(), results);
601
602 let network_id = 1;
603 let network = test_network_message_from_id(network_id);
604
605 assert_matches!(
607 manager.update_network(network.clone()),
608 Err(errno) if errno.code.error_code() == ENOENT
609 );
610
611 {
613 let mut inner_guard = manager.lock();
614 let _ = inner_guard.as_mut().networks.insert(network_id, Some(network.clone()));
615 }
616
617 assert_matches!(manager.update_network(network.clone()), Ok(()));
623 assert_matches!(manager.update_network(network), Ok(()));
624
625 assert_data_tree!(inspector, root: {
626 nmfs: contains {
627 default_ids_set: {
628 seen: 0u64,
629 sent: 0u64,
630 },
631 added_networks: {
632 seen: 0u64,
633 sent: 0u64,
634 },
635 updated_networks: {
636 seen: expected_data.seen,
637 sent: expected_data.sent,
638 },
639 removed_networks: {
640 seen: 0u64,
641 sent: 0u64,
642 },
643 },
644 });
645 }
646
647 #[test_case(vec![Ok(())], SeenSentData { seen: 1, sent: 1 }; "success")]
648 #[test_case(
649 vec![
650 Err(NetworkManagerError::Remove(fnp_properties::NetworkRegistryRemoveError::NotFound)),
651 ],
652 SeenSentData { seen: 1, sent: 0 };
653 "error"
654 )]
655 #[::fuchsia::test(threads = 2)]
656 async fn test_remove_network_with_proxy(
657 results: Vec<Result<(), NetworkManagerError>>,
658 expected_data: SeenSentData,
659 ) {
660 let inspector = fuchsia_inspect::Inspector::default();
661 let manager = &setup_proxy(inspector.root(), results);
662
663 let network_id = 1;
664 let network = test_network_message_from_id(network_id);
665
666 assert_matches!(
668 manager.remove_network(network_id),
669 Err(errno) if errno.code.error_code() == ENOENT
670 );
671
672 {
674 let mut inner_guard = manager.lock();
675 let _ = inner_guard.as_mut().networks.insert(network_id, Some(network.clone()));
676 }
677
678 assert_matches!(manager.remove_network(network_id), Ok(()));
682
683 assert_data_tree!(inspector, root: {
684 nmfs: contains {
685 default_ids_set: {
686 seen: 0u64,
687 sent: 0u64,
688 },
689 added_networks: {
690 seen: 0u64,
691 sent: 0u64,
692 },
693 updated_networks: {
694 seen: 0u64,
695 sent: 0u64,
696 },
697 removed_networks: {
698 seen: expected_data.seen,
699 sent: expected_data.sent,
700 },
701 },
702 });
703 }
704
705 #[::fuchsia::test(threads = 2)]
706 async fn test_multiple_operations_with_proxy() {
707 let inspector = fuchsia_inspect::Inspector::default();
708 let results = vec![
709 Err(NetworkManagerError::Add(
711 fnp_properties::NetworkRegistryAddError::MissingNetworkId,
712 )),
713 Ok(()),
715 Err(NetworkManagerError::SetDefault(
717 fnp_properties::NetworkRegistrySetDefaultError::NotFound,
718 )),
719 Ok(()),
721 Err(NetworkManagerError::Update(fnp_properties::NetworkRegistryUpdateError::NotFound)),
723 Ok(()),
725 Ok(()),
727 Err(NetworkManagerError::Remove(fnp_properties::NetworkRegistryRemoveError::NotFound)),
729 Ok(()),
731 ];
732 let manager = &setup_proxy(inspector.root(), results);
733
734 let network1 = test_network_message_from_id(1);
736 assert_matches!(manager.add_network(network1.clone()), Ok(()));
737 assert_data_tree!(inspector, root: {
738 nmfs: contains {
739 added_networks: {
740 seen: 1u64,
741 sent: 0u64,
742 },
743 },
744 });
745
746 let network2 = test_network_message_from_id(2);
748 assert_matches!(manager.add_network(network2.clone()), Ok(()));
749 assert_data_tree!(inspector, root: {
750 nmfs: contains {
751 added_networks: {
752 seen: 2u64,
753 sent: 1u64,
754 },
755 },
756 });
757
758 manager.set_default_network_id(Some(1));
760 assert_eq!(manager.get_default_network_id(), Some(1));
761 assert_data_tree!(inspector, root: {
762 nmfs: contains {
763 default_ids_set: {
764 seen: 1u64,
765 sent: 0u64,
766 },
767 },
768 });
769
770 manager.set_default_network_id(Some(2));
772 assert_eq!(manager.get_default_network_id(), Some(2));
773 assert_data_tree!(inspector, root: {
774 nmfs: contains {
775 default_ids_set: {
776 seen: 2u64,
777 sent: 1u64,
778 },
779 },
780 });
781
782 let mut network1_updated = network1.clone();
784 network1_updated.mark = 1;
785 assert_matches!(manager.update_network(network1_updated.clone()), Ok(()));
786 assert_data_tree!(inspector, root: {
787 nmfs: contains {
788 updated_networks: {
789 seen: 1u64,
790 sent: 0u64,
791 },
792 },
793 });
794
795 let mut network2_updated = network2.clone();
797 network2_updated.mark = 2;
798 assert_matches!(manager.update_network(network2_updated.clone()), Ok(()));
799 assert_data_tree!(inspector, root: {
800 nmfs: contains {
801 updated_networks: {
802 seen: 2u64,
803 sent: 1u64,
804 },
805 },
806 });
807
808 manager.set_default_network_id(None);
810 assert_data_tree!(inspector, root: {
811 nmfs: contains {
812 default_ids_set: {
813 seen: 3u64,
814 sent: 2u64,
815 },
816 },
817 });
818
819 assert_matches!(manager.remove_network(1), Ok(()));
821 assert_data_tree!(inspector, root: {
822 nmfs: contains {
823 removed_networks: {
824 seen: 1u64,
825 sent: 0u64,
826 },
827 },
828 });
829
830 assert_matches!(manager.remove_network(2), Ok(()));
832 assert_data_tree!(inspector, root: {
833 nmfs: contains {
834 default_ids_set: {
835 seen: 3u64,
836 sent: 2u64,
837 },
838 added_networks: {
839 seen: 2u64,
840 sent: 1u64,
841 },
842 updated_networks: {
843 seen: 2u64,
844 sent: 1u64,
845 },
846 removed_networks: {
847 seen: 2u64,
848 sent: 1u64,
849 },
850 },
851 });
852 }
853}