1use crate::error::ParserError;
48use crate::models::*;
49use crate::parser::iters::write_mrt_core_dump;
50use crate::parser::BgpkitParser;
51use crate::Elementor;
52use log::{error, warn};
53use std::io::Read;
54use std::net::IpAddr;
55
56#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct Bgp4MpUpdate {
64 pub timestamp: f64,
66 pub peer_ip: IpAddr,
68 pub peer_asn: Asn,
70 pub message: BgpUpdateMessage,
72}
73
74#[derive(Debug, Clone, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub struct LegacyBgpUpdate {
84 pub timestamp: f64,
85 pub peer_ip: IpAddr,
86 pub peer_asn: Asn,
87 pub local_ip: IpAddr,
88 pub local_asn: Asn,
89 pub message: BgpUpdateMessage,
90}
91
92#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct TableDumpV2Entry {
99 pub timestamp: f64,
101 pub rib_type: TableDumpV2Type,
103 pub sequence_number: u32,
105 pub prefix: NetworkPrefix,
107 pub rib_entries: Vec<RibEntry>,
110}
111
112#[derive(Debug, Clone, PartialEq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120pub enum MrtUpdate {
121 Bgp4MpUpdate(Bgp4MpUpdate),
123 LegacyBgpUpdate(LegacyBgpUpdate),
125 TableDumpV2Entry(TableDumpV2Entry),
127 TableDumpMessage(TableDumpMessage),
129}
130
131impl MrtUpdate {
132 pub fn timestamp(&self) -> f64 {
134 match self {
135 MrtUpdate::Bgp4MpUpdate(u) => u.timestamp,
136 MrtUpdate::LegacyBgpUpdate(u) => u.timestamp,
137 MrtUpdate::TableDumpV2Entry(e) => e.timestamp,
138 MrtUpdate::TableDumpMessage(m) => m.originated_time as f64,
139 }
140 }
141}
142
143pub struct UpdateIterator<R> {
152 parser: BgpkitParser<R>,
153 elementor: Elementor,
154 pending_table_dump: Vec<TableDumpMessage>,
155}
156
157impl<R> UpdateIterator<R> {
158 pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
159 UpdateIterator {
160 parser,
161 elementor: Elementor::new(),
162 pending_table_dump: Vec::new(),
163 }
164 }
165}
166
167impl<R: Read> Iterator for UpdateIterator<R> {
168 type Item = MrtUpdate;
169
170 fn next(&mut self) -> Option<MrtUpdate> {
171 if self.parser.text_dump_iter.is_some() {
174 return None;
175 }
176 loop {
177 if let Some(message) = self.pending_table_dump.pop() {
178 return Some(MrtUpdate::TableDumpMessage(message));
179 }
180 let record = match self.parser.next_record() {
181 Ok(record) => record,
182 Err(e) => match e.error {
183 ParserError::TruncatedMsg(err_str) | ParserError::Unsupported(err_str) => {
184 if self.parser.options.show_warnings {
185 warn!("parser warn: {}", err_str);
186 }
187 write_mrt_core_dump(self.parser.core_dump, e.bytes);
188 continue;
189 }
190 ParserError::ParseError(err_str) => {
191 error!("parser error: {}", err_str);
192 write_mrt_core_dump(self.parser.core_dump, e.bytes);
193 if self.parser.core_dump {
194 return None;
195 }
196 continue;
197 }
198 ParserError::EofExpected => return None,
199 ParserError::IoError(err) | ParserError::EofError(err) => {
200 error!("{:?}", err);
201 write_mrt_core_dump(self.parser.core_dump, e.bytes);
202 return None;
203 }
204 #[cfg(feature = "oneio")]
205 ParserError::OneIoError(_) => return None,
206 ParserError::FilterError(_) => return None,
207 ParserError::InvalidLabeledNlriLength
209 | ParserError::TruncatedLabeledNlri
210 | ParserError::TruncatedPrefix
211 | ParserError::MaxLabelStackDepthExceeded
212 | ParserError::PeerMaxLabelsExceeded
213 | ParserError::InvalidPrefix => {
214 if self.parser.options.show_warnings {
215 warn!("parser warn: labeled NLRI parsing error: {:?}", e.error);
216 }
217 continue;
218 }
219 },
220 };
221
222 let t = record.common_header.timestamp;
223 let timestamp: f64 = if let Some(micro) = &record.common_header.microsecond_timestamp {
224 let m = (*micro as f64) / 1_000_000.0;
225 t as f64 + m
226 } else {
227 f64::from(t)
228 };
229
230 match record.message {
231 MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(msg)) => {
232 if let BgpMessage::Update(update) = msg.bgp_message {
233 return Some(MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
234 timestamp,
235 peer_ip: msg.peer_ip,
236 peer_asn: msg.peer_asn,
237 message: update,
238 }));
239 }
240 continue;
242 }
243 MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(_)) => {
244 continue;
246 }
247 MrtMessage::LegacyBgp(LegacyBgp::Message(msg)) => {
248 if let BgpMessage::Update(update) = msg.bgp_message {
249 return Some(MrtUpdate::LegacyBgpUpdate(LegacyBgpUpdate {
250 timestamp,
251 peer_ip: msg.peer_ip,
252 peer_asn: msg.peer_asn,
253 local_ip: msg.local_ip,
254 local_asn: msg.local_asn,
255 message: update,
256 }));
257 }
258 continue;
259 }
260 MrtMessage::LegacyBgp(LegacyBgp::StateChange(_)) => continue,
261 MrtMessage::TableDumpV2Message(msg) => {
262 match msg {
263 TableDumpV2Message::PeerIndexTable(p) => {
264 self.elementor.peer_table = Some(p);
266 continue;
267 }
268 TableDumpV2Message::RibAfi(entries) => {
269 return Some(MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
270 timestamp,
271 rib_type: entries.rib_type,
272 sequence_number: entries.sequence_number,
273 prefix: entries.prefix,
274 rib_entries: entries.rib_entries,
275 }));
276 }
277 TableDumpV2Message::RibGeneric(_) => {
278 continue;
280 }
281 TableDumpV2Message::GeoPeerTable(_) => {
282 continue;
284 }
285 }
286 }
287 MrtMessage::TableDumpMessage(msg) => {
288 return Some(MrtUpdate::TableDumpMessage(msg));
289 }
290 MrtMessage::TableDumpMessageBatch(mut messages) => {
291 messages.reverse();
292 self.pending_table_dump = messages;
293 continue;
294 }
295 }
296 }
297 }
298}
299
300pub struct FallibleUpdateIterator<R> {
305 parser: BgpkitParser<R>,
306 elementor: Elementor,
307 pending_table_dump: Vec<TableDumpMessage>,
308}
309
310impl<R> FallibleUpdateIterator<R> {
311 pub(crate) fn new(parser: BgpkitParser<R>) -> Self {
312 FallibleUpdateIterator {
313 parser,
314 elementor: Elementor::new(),
315 pending_table_dump: Vec::new(),
316 }
317 }
318}
319
320impl<R: Read> Iterator for FallibleUpdateIterator<R> {
321 type Item = Result<MrtUpdate, crate::error::ParserErrorWithBytes>;
322
323 fn next(&mut self) -> Option<Self::Item> {
324 if self.parser.text_dump_iter.is_some() {
326 return None;
327 }
328 loop {
329 if let Some(message) = self.pending_table_dump.pop() {
330 return Some(Ok(MrtUpdate::TableDumpMessage(message)));
331 }
332 match self.parser.next_record() {
333 Ok(record) => {
334 let t = record.common_header.timestamp;
335 let timestamp: f64 =
336 if let Some(micro) = &record.common_header.microsecond_timestamp {
337 let m = (*micro as f64) / 1_000_000.0;
338 t as f64 + m
339 } else {
340 f64::from(t)
341 };
342
343 match record.message {
344 MrtMessage::Bgp4Mp(Bgp4MpEnum::Message(msg)) => {
345 if let BgpMessage::Update(update) = msg.bgp_message {
346 return Some(Ok(MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
347 timestamp,
348 peer_ip: msg.peer_ip,
349 peer_asn: msg.peer_asn,
350 message: update,
351 })));
352 }
353 continue;
354 }
355 MrtMessage::Bgp4Mp(Bgp4MpEnum::StateChange(_)) => {
356 continue;
357 }
358 MrtMessage::LegacyBgp(LegacyBgp::Message(msg)) => {
359 if let BgpMessage::Update(update) = msg.bgp_message {
360 return Some(Ok(MrtUpdate::LegacyBgpUpdate(LegacyBgpUpdate {
361 timestamp,
362 peer_ip: msg.peer_ip,
363 peer_asn: msg.peer_asn,
364 local_ip: msg.local_ip,
365 local_asn: msg.local_asn,
366 message: update,
367 })));
368 }
369 continue;
370 }
371 MrtMessage::LegacyBgp(LegacyBgp::StateChange(_)) => continue,
372 MrtMessage::TableDumpV2Message(msg) => match msg {
373 TableDumpV2Message::PeerIndexTable(p) => {
374 self.elementor.peer_table = Some(p);
375 continue;
376 }
377 TableDumpV2Message::RibAfi(entries) => {
378 return Some(Ok(MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
379 timestamp,
380 rib_type: entries.rib_type,
381 sequence_number: entries.sequence_number,
382 prefix: entries.prefix,
383 rib_entries: entries.rib_entries,
384 })));
385 }
386 TableDumpV2Message::RibGeneric(_) => {
387 continue;
388 }
389 TableDumpV2Message::GeoPeerTable(_) => {
390 continue;
391 }
392 },
393 MrtMessage::TableDumpMessage(msg) => {
394 return Some(Ok(MrtUpdate::TableDumpMessage(msg)));
395 }
396 MrtMessage::TableDumpMessageBatch(mut messages) => {
397 messages.reverse();
398 self.pending_table_dump = messages;
399 continue;
400 }
401 }
402 }
403 Err(e) if matches!(e.error, ParserError::EofExpected) => {
404 return None;
405 }
406 Err(e) => {
407 return Some(Err(e));
408 }
409 }
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use std::io::Cursor;
418
419 #[test]
420 fn test_bgp4mp_update_struct() {
421 let update = Bgp4MpUpdate {
422 timestamp: 1234567890.123456,
423 peer_ip: "192.0.2.1".parse().unwrap(),
424 peer_asn: Asn::new_32bit(65000),
425 message: BgpUpdateMessage::default(),
426 };
427
428 assert_eq!(update.timestamp, 1234567890.123456);
429 assert_eq!(update.peer_ip.to_string(), "192.0.2.1");
430 assert_eq!(update.peer_asn, Asn::new_32bit(65000));
431 }
432
433 #[test]
434 fn test_table_dump_v2_entry_struct() {
435 let entry = TableDumpV2Entry {
436 timestamp: 1234567890.0,
437 rib_type: TableDumpV2Type::RibIpv4Unicast,
438 sequence_number: 42,
439 prefix: "10.0.0.0/8".parse().unwrap(),
440 rib_entries: vec![],
441 };
442
443 assert_eq!(entry.timestamp, 1234567890.0);
444 assert_eq!(entry.rib_type, TableDumpV2Type::RibIpv4Unicast);
445 assert_eq!(entry.sequence_number, 42);
446 assert_eq!(entry.prefix.to_string(), "10.0.0.0/8");
447 assert!(entry.rib_entries.is_empty());
448 }
449
450 #[test]
451 fn test_mrt_update_timestamp() {
452 let bgp4mp = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
454 timestamp: 1234567890.5,
455 peer_ip: "192.0.2.1".parse().unwrap(),
456 peer_asn: Asn::new_32bit(65000),
457 message: BgpUpdateMessage::default(),
458 });
459 assert_eq!(bgp4mp.timestamp(), 1234567890.5);
460
461 let table_dump_v2 = MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
463 timestamp: 1234567891.5,
464 rib_type: TableDumpV2Type::RibIpv4Unicast,
465 sequence_number: 1,
466 prefix: "10.0.0.0/8".parse().unwrap(),
467 rib_entries: vec![],
468 });
469 assert_eq!(table_dump_v2.timestamp(), 1234567891.5);
470
471 let table_dump_v1 = MrtUpdate::TableDumpMessage(TableDumpMessage {
473 view_number: 0,
474 sequence_number: 1,
475 prefix: "192.168.0.0/16".parse().unwrap(),
476 status: 1,
477 originated_time: 1234567892,
478 peer_ip: "10.0.0.1".parse().unwrap(),
479 peer_asn: Asn::new_32bit(65001),
480 attributes: Attributes::default(),
481 });
482 assert_eq!(table_dump_v1.timestamp(), 1234567892.0);
483 }
484
485 #[test]
486 fn test_update_iterator_empty() {
487 let cursor = Cursor::new(vec![]);
488 let parser = BgpkitParser::from_reader(cursor);
489 let mut iter = UpdateIterator::new(parser);
490
491 assert!(iter.next().is_none());
492 }
493
494 #[test]
495 fn test_fallible_update_iterator_empty() {
496 let cursor = Cursor::new(vec![]);
497 let parser = BgpkitParser::from_reader(cursor);
498 let mut iter = FallibleUpdateIterator::new(parser);
499
500 assert!(iter.next().is_none());
501 }
502
503 #[test]
504 fn test_bgp4mp_update_clone_and_debug() {
505 let update = Bgp4MpUpdate {
506 timestamp: 1234567890.123456,
507 peer_ip: "192.0.2.1".parse().unwrap(),
508 peer_asn: Asn::new_32bit(65000),
509 message: BgpUpdateMessage::default(),
510 };
511
512 let cloned = update.clone();
514 assert_eq!(update, cloned);
515
516 let debug_str = format!("{:?}", update);
518 assert!(debug_str.contains("Bgp4MpUpdate"));
519 assert!(debug_str.contains("192.0.2.1"));
520 }
521
522 #[test]
523 fn test_table_dump_v2_entry_clone_and_debug() {
524 let entry = TableDumpV2Entry {
525 timestamp: 1234567890.0,
526 rib_type: TableDumpV2Type::RibIpv4Unicast,
527 sequence_number: 42,
528 prefix: "10.0.0.0/8".parse().unwrap(),
529 rib_entries: vec![],
530 };
531
532 let cloned = entry.clone();
534 assert_eq!(entry, cloned);
535
536 let debug_str = format!("{:?}", entry);
538 assert!(debug_str.contains("TableDumpV2Entry"));
539 assert!(debug_str.contains("10.0.0.0/8"));
540 }
541
542 #[test]
543 fn test_mrt_update_clone_and_debug() {
544 let update = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
545 timestamp: 1234567890.5,
546 peer_ip: "192.0.2.1".parse().unwrap(),
547 peer_asn: Asn::new_32bit(65000),
548 message: BgpUpdateMessage::default(),
549 });
550
551 let cloned = update.clone();
553 assert_eq!(update, cloned);
554
555 let debug_str = format!("{:?}", update);
557 assert!(debug_str.contains("Bgp4MpUpdate"));
558 }
559
560 #[test]
561 fn test_fallible_update_iterator_with_invalid_data() {
562 let invalid_data = vec![
564 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, ];
570
571 let cursor = Cursor::new(invalid_data);
572 let parser = BgpkitParser::from_reader(cursor);
573 let mut iter = FallibleUpdateIterator::new(parser);
574
575 let result = iter.next();
577 assert!(result.is_some());
578 assert!(result.unwrap().is_err());
579 }
580
581 #[test]
582 fn test_mrt_update_enum_variants() {
583 let updates: Vec<MrtUpdate> = vec![
585 MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
586 timestamp: 1.0,
587 peer_ip: "192.0.2.1".parse().unwrap(),
588 peer_asn: Asn::new_32bit(65000),
589 message: BgpUpdateMessage::default(),
590 }),
591 MrtUpdate::TableDumpV2Entry(TableDumpV2Entry {
592 timestamp: 2.0,
593 rib_type: TableDumpV2Type::RibIpv6Unicast,
594 sequence_number: 1,
595 prefix: "2001:db8::/32".parse().unwrap(),
596 rib_entries: vec![],
597 }),
598 MrtUpdate::TableDumpMessage(TableDumpMessage {
599 view_number: 0,
600 sequence_number: 1,
601 prefix: "10.0.0.0/8".parse().unwrap(),
602 status: 1,
603 originated_time: 3,
604 peer_ip: "10.0.0.1".parse().unwrap(),
605 peer_asn: Asn::new_32bit(65001),
606 attributes: Attributes::default(),
607 }),
608 ];
609
610 for (i, update) in updates.iter().enumerate() {
611 match update {
612 MrtUpdate::Bgp4MpUpdate(_) => assert_eq!(i, 0),
613 MrtUpdate::LegacyBgpUpdate(_) => panic!("not part of this fixture"),
614 MrtUpdate::TableDumpV2Entry(_) => assert_eq!(i, 1),
615 MrtUpdate::TableDumpMessage(_) => assert_eq!(i, 2),
616 }
617 }
618 }
619
620 #[test]
621 #[cfg(feature = "serde")]
622 fn test_bgp4mp_update_serde() {
623 let update = Bgp4MpUpdate {
624 timestamp: 1234567890.123456,
625 peer_ip: "192.0.2.1".parse().unwrap(),
626 peer_asn: Asn::new_32bit(65000),
627 message: BgpUpdateMessage::default(),
628 };
629
630 let serialized = serde_json::to_string(&update).unwrap();
631 let deserialized: Bgp4MpUpdate = serde_json::from_str(&serialized).unwrap();
632 assert_eq!(update, deserialized);
633 }
634
635 #[test]
636 #[cfg(feature = "serde")]
637 fn test_table_dump_v2_entry_serde() {
638 let entry = TableDumpV2Entry {
639 timestamp: 1234567890.0,
640 rib_type: TableDumpV2Type::RibIpv4Unicast,
641 sequence_number: 42,
642 prefix: "10.0.0.0/8".parse().unwrap(),
643 rib_entries: vec![],
644 };
645
646 let serialized = serde_json::to_string(&entry).unwrap();
647 let deserialized: TableDumpV2Entry = serde_json::from_str(&serialized).unwrap();
648 assert_eq!(entry, deserialized);
649 }
650
651 #[test]
652 #[cfg(feature = "serde")]
653 fn test_mrt_update_serde() {
654 let update = MrtUpdate::Bgp4MpUpdate(Bgp4MpUpdate {
655 timestamp: 1234567890.5,
656 peer_ip: "192.0.2.1".parse().unwrap(),
657 peer_asn: Asn::new_32bit(65000),
658 message: BgpUpdateMessage::default(),
659 });
660
661 let serialized = serde_json::to_string(&update).unwrap();
662 let deserialized: MrtUpdate = serde_json::from_str(&serialized).unwrap();
663 assert_eq!(update, deserialized);
664 }
665
666 #[test]
668 fn test_update_iterator_with_updates_file() {
669 let url = "https://spaces.bgpkit.org/parser/update-example";
670 let parser = BgpkitParser::new(url).unwrap();
671
672 let mut bgp4mp_count = 0;
673 let mut total_announced = 0;
674 let mut total_withdrawn = 0;
675
676 for update in parser.into_update_iter() {
677 match update {
678 MrtUpdate::Bgp4MpUpdate(u) => {
679 bgp4mp_count += 1;
680 total_announced += u.message.announced_prefixes.len();
681 total_withdrawn += u.message.withdrawn_prefixes.len();
682 for attr in &u.message.attributes {
684 match attr {
685 AttributeValue::MpReachNlri(nlri) => {
686 total_announced += nlri.prefixes.len();
687 }
688 AttributeValue::MpUnreachNlri(nlri) => {
689 total_withdrawn += nlri.prefixes.len();
690 }
691 _ => {}
692 }
693 }
694 }
695 MrtUpdate::TableDumpV2Entry(_) => {
696 panic!("Should not see TableDumpV2Entry in UPDATES file");
697 }
698 MrtUpdate::TableDumpMessage(_) => {
699 panic!("Should not see TableDumpMessage in UPDATES file");
700 }
701 MrtUpdate::LegacyBgpUpdate(_) => {
702 panic!("Should not see a legacy BGP update in this modern fixture");
703 }
704 }
705 }
706
707 assert!(bgp4mp_count > 0, "Should have parsed some BGP4MP updates");
709 assert!(
710 total_announced + total_withdrawn > 0,
711 "Should have some prefixes"
712 );
713 }
714
715 #[test]
717 fn test_update_iterator_with_rib_file() {
718 let url = "https://spaces.bgpkit.org/parser/rib-example-small.bz2";
719 let parser = BgpkitParser::new(url).unwrap();
720
721 let mut rib_entry_count = 0;
722 let mut total_rib_entries = 0;
723
724 for update in parser.into_update_iter().take(100) {
725 match update {
726 MrtUpdate::Bgp4MpUpdate(_) => {
727 panic!("Should not see Bgp4MpUpdate in RIB file");
728 }
729 MrtUpdate::LegacyBgpUpdate(_) => {
730 panic!("Should not see LegacyBgpUpdate in RIB file");
731 }
732 MrtUpdate::TableDumpV2Entry(e) => {
733 rib_entry_count += 1;
734 total_rib_entries += e.rib_entries.len();
735 assert!(e.sequence_number > 0 || rib_entry_count == 1);
737 }
738 MrtUpdate::TableDumpMessage(_) => {
739 }
741 }
742 }
743
744 assert!(rib_entry_count > 0, "Should have parsed some RIB entries");
746 assert!(
747 total_rib_entries > 0,
748 "Should have some RIB entries per prefix"
749 );
750 }
751
752 #[test]
754 fn test_fallible_update_iterator_with_updates_file() {
755 let url = "https://spaces.bgpkit.org/parser/update-example";
756 let parser = BgpkitParser::new(url).unwrap();
757
758 let mut success_count = 0;
759 let mut error_count = 0;
760
761 for result in parser.into_fallible_update_iter() {
762 match result {
763 Ok(_) => success_count += 1,
764 Err(_) => error_count += 1,
765 }
766 }
767
768 assert!(
769 success_count > 0,
770 "Should have parsed some updates successfully"
771 );
772 assert_eq!(
774 error_count, 0,
775 "Should have no parsing errors in valid file"
776 );
777 }
778
779 #[test]
781 fn test_update_iter_vs_elem_iter_consistency() {
782 let url = "https://spaces.bgpkit.org/parser/update-example";
783
784 let parser1 = BgpkitParser::new(url).unwrap();
786 let mut update_iter_announced = 0;
787 let mut update_iter_withdrawn = 0;
788
789 for update in parser1.into_update_iter() {
790 if let MrtUpdate::Bgp4MpUpdate(u) = update {
791 update_iter_announced += u.message.announced_prefixes.len();
792 update_iter_withdrawn += u.message.withdrawn_prefixes.len();
793 for attr in &u.message.attributes {
794 match attr {
795 AttributeValue::MpReachNlri(nlri) => {
796 update_iter_announced += nlri.prefixes.len();
797 }
798 AttributeValue::MpUnreachNlri(nlri) => {
799 update_iter_withdrawn += nlri.prefixes.len();
800 }
801 _ => {}
802 }
803 }
804 }
805 }
806
807 let parser2 = BgpkitParser::new(url).unwrap();
809 let mut elem_iter_announced = 0;
810 let mut elem_iter_withdrawn = 0;
811
812 for elem in parser2.into_elem_iter() {
813 match elem.elem_type {
814 ElemType::ANNOUNCE => elem_iter_announced += 1,
815 ElemType::WITHDRAW => elem_iter_withdrawn += 1,
816 }
817 }
818
819 assert_eq!(
821 update_iter_announced, elem_iter_announced,
822 "Announced prefix counts should match"
823 );
824 assert_eq!(
825 update_iter_withdrawn, elem_iter_withdrawn,
826 "Withdrawn prefix counts should match"
827 );
828 }
829}