1use crate::events::Event;
24use crate::mls_observability::MlsLifecycleEvent;
25use crate::telemetry::device::DeviceCapabilitySnapshot;
26use crate::telemetry::metrics_snapshot::MetricsFrame;
27use crate::telemetry::routing::RoutingDecision;
28use crate::telemetry::transport_state::TransportStateEvent;
29
30#[non_exhaustive]
35#[derive(Debug, Clone)]
36pub enum TelemetryRecord {
37 Protocol(Box<Event>),
44 Mls(MlsLifecycleEvent),
46 MetricsSnapshot(Box<MetricsFrame>),
51 TransportState(TransportStateEvent),
54 Routing(Box<RoutingDecision>),
59 Device(DeviceCapabilitySnapshot),
61}
62
63impl TelemetryRecord {
64 pub fn name(&self) -> &'static str {
67 match self {
68 TelemetryRecord::Protocol(event) => event.telemetry_name(),
69 TelemetryRecord::Mls(event) => event.telemetry_name(),
70 TelemetryRecord::MetricsSnapshot(frame) => frame.telemetry_name(),
71 TelemetryRecord::TransportState(event) => event.telemetry_name(),
72 TelemetryRecord::Routing(decision) => decision.telemetry_name(),
73 TelemetryRecord::Device(snapshot) => snapshot.telemetry_name(),
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::events::{
82 DorsEscalationPhase, DorsEscalationReasonCode, DorsReasonCode, PresenceSource,
83 PresenceStatus, SecurityWarningCode, WelcomeReasonCode,
84 };
85 use crate::mls_observability::{DecryptionFailureKind, MlsOperationContext};
86 use std::collections::{HashMap, HashSet};
87
88 const ALL_TELEMETRY_NAMES: &[&str] = &[
109 "protocol.message.sent",
111 "protocol.message.received",
112 "protocol.message.delivered",
113 "protocol.message.failed",
114 "protocol.message.decryption_failed",
115 "protocol.transport.switched",
116 "protocol.relay.promoted",
117 "protocol.relay.demoted",
118 "protocol.neighbor.discovered",
119 "protocol.neighbor.lost",
120 "protocol.identity.ready",
121 "protocol.network.metrics",
122 "protocol.file.progress",
123 "protocol.file.received",
124 "protocol.file.receive_failed",
125 "protocol.media.sent",
126 "protocol.media.send_failed",
127 "protocol.message.deferred",
128 "protocol.message.retrying",
129 "protocol.message.undeliverable",
130 "protocol.media.resend_required",
131 "protocol.ack.evicted",
132 "protocol.fragment.assembly_evicted",
133 "protocol.relay.demoted_battery",
134 "protocol.secure_session.established",
135 "protocol.secure_session.failed",
136 "protocol.convergence.diag",
137 "protocol.welcome.send_attempted",
138 "protocol.welcome.send_succeeded",
139 "protocol.welcome.send_failed",
140 "protocol.welcome.send_expired",
141 "protocol.connection.request_received",
142 "protocol.connection.request_undeliverable",
143 "protocol.connection.accepted",
144 "protocol.connection.rejected",
145 "protocol.connection.request_cancelled",
146 "protocol.group.created",
147 "protocol.group.message_received",
148 "protocol.group.member_added",
149 "protocol.group.member_removed",
150 "protocol.group.info",
151 "protocol.group.user_groups",
152 "protocol.group.error",
153 "protocol.group.relay_sync_changed",
154 "protocol.group.message_sent",
155 "protocol.group.message_partial_failure",
156 "protocol.group.delivery_report",
157 "protocol.group.rich_extras_dropped",
158 "protocol.group.unauthorized_membership_change",
159 "protocol.group.epoch_fork_detected",
160 "protocol.group.epoch_fork_resolved",
161 "protocol.group.role_changed",
162 "protocol.group.renamed",
163 "protocol.service.discovered",
164 "protocol.service.request_received",
165 "protocol.service.response_received",
166 "protocol.presence.updated",
167 "protocol.typing.received",
168 "protocol.read_receipt.received",
169 "protocol.dors.score_updated",
170 "protocol.dors.transport_selected",
171 "protocol.dors.transport_switched",
172 "protocol.dors.escalation_triggered",
173 "protocol.security.warning",
174 "protocol.message.relayed",
175 "protocol.user.blocked",
176 "protocol.user.unblocked",
177 "mls.initialized",
179 "mls.encryption_used",
180 "mls.decryption_failed",
181 "mls.session_missing",
182 "mls.session_ready",
183 "protocol.metrics.snapshot",
192 "protocol.transport.state_changed",
193 "protocol.routing.decision",
194 "protocol.device.capability_changed",
195 ];
196
197 fn is_well_formed(name: &str) -> bool {
200 if name.is_empty() {
201 return false;
202 }
203 let mut segments = name.split('.');
204 let mut segment_count = 0;
205 for segment in &mut segments {
206 if segment.is_empty() {
207 return false;
208 }
209 if !segment
210 .chars()
211 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
212 {
213 return false;
214 }
215 segment_count += 1;
216 }
217 segment_count >= 2
218 }
219
220 #[test]
227 fn telemetry_record_is_smaller_than_unboxed_event() {
228 use std::mem::size_of;
229 assert!(
230 size_of::<TelemetryRecord>() < size_of::<Event>(),
231 "TelemetryRecord ({} bytes) is not smaller than Event ({} bytes) \
232 — the `Box<Event>` variant was removed or the size budget was \
233 blown by another variant. Re-box the Protocol variant or audit \
234 the new variant's payload.",
235 size_of::<TelemetryRecord>(),
236 size_of::<Event>(),
237 );
238 }
239
240 #[test]
241 fn protocol_variant_names_are_dotted() {
242 let event = Event::MessageReceived {
243 message_id: "m".into(),
244 sender: "s".into(),
245 recipient: "r".into(),
246 content: String::new(),
247 reply_context: None,
248 hop_count: 0,
249 transport: "ble".into(),
250 timestamp: 0,
251 lamport_clock: 0,
252 reply_to_msg: None,
253 content_type: String::new(),
254 media_metadata: None,
255 forward_info: None,
256 encrypted: false,
257 };
258 assert_eq!(
259 TelemetryRecord::Protocol(Box::new(event)).name(),
260 "protocol.message.received",
261 );
262 }
263
264 #[test]
265 fn mls_variant_names_are_dotted() {
266 let event = MlsLifecycleEvent::Initialized {
267 timestamp_ms: 0,
268 session_id: "s".into(),
269 group_id: None,
270 peer_id: None,
271 context: MlsOperationContext::Initialize,
272 error_category: None,
273 };
274 assert_eq!(TelemetryRecord::Mls(event).name(), "mls.initialized");
275 }
276
277 #[allow(dead_code)]
286 fn event_variant_exhaustiveness_ward(e: &Event) {
287 match e {
288 Event::MessageSent { .. }
289 | Event::MessageReceived { .. }
290 | Event::MessageDelivered { .. }
291 | Event::MessageFailed { .. }
292 | Event::MessageDecryptionFailed { .. }
293 | Event::TransportSwitched { .. }
294 | Event::RelayPromoted { .. }
295 | Event::RelayDemoted { .. }
296 | Event::NeighborDiscovered { .. }
297 | Event::NeighborLost { .. }
298 | Event::IdentityReady { .. }
299 | Event::NetworkMetrics { .. }
300 | Event::FileProgress { .. }
301 | Event::FileReceived { .. }
302 | Event::FileReceiveFailed { .. }
303 | Event::MediaSent { .. }
304 | Event::MediaSendFailed { .. }
305 | Event::MessageDeferred { .. }
306 | Event::MessageRetrying { .. }
307 | Event::MessageUndeliverable { .. }
308 | Event::MediaResendRequired { .. }
309 | Event::AckEvicted { .. }
310 | Event::FragmentAssemblyEvicted { .. }
311 | Event::RelayDemotedBattery { .. }
312 | Event::SecureSessionEstablished { .. }
313 | Event::SecureSessionFailed { .. }
314 | Event::ConvergenceDiag { .. }
315 | Event::WelcomeSendAttempted { .. }
316 | Event::WelcomeSendSucceeded { .. }
317 | Event::WelcomeSendFailed { .. }
318 | Event::WelcomeSendExpired { .. }
319 | Event::ConnectionRequestReceived { .. }
320 | Event::ConnectionRequestUndeliverable { .. }
321 | Event::ConnectionAccepted { .. }
322 | Event::ConnectionRejected { .. }
323 | Event::ConnectionRequestCancelled { .. }
324 | Event::GroupCreated { .. }
325 | Event::GroupMessageReceived { .. }
326 | Event::GroupMemberAdded { .. }
327 | Event::GroupMemberRemoved { .. }
328 | Event::GroupInfo { .. }
329 | Event::UserGroups { .. }
330 | Event::GroupError { .. }
331 | Event::GroupRelaySyncChanged { .. }
332 | Event::GroupMessageSent { .. }
333 | Event::GroupMessagePartialFailure { .. }
334 | Event::GroupMessageDeliveryReport { .. }
335 | Event::GroupRichExtrasDropped { .. }
336 | Event::GroupUnauthorizedMembershipChange { .. }
337 | Event::GroupEpochForkDetected { .. }
338 | Event::GroupEpochForkResolved { .. }
339 | Event::GroupRoleChanged { .. }
340 | Event::GroupRenamed { .. }
341 | Event::ServiceDiscovered { .. }
342 | Event::ServiceRequestReceived { .. }
343 | Event::ServiceResponseReceived { .. }
344 | Event::PresenceUpdated { .. }
345 | Event::TypingIndicatorReceived { .. }
346 | Event::ReadReceiptReceived { .. }
347 | Event::DorsScoreUpdated { .. }
348 | Event::DorsTransportSelected { .. }
349 | Event::DorsTransportSwitched { .. }
350 | Event::DorsEscalationTriggered { .. }
351 | Event::SecurityWarning { .. }
352 | Event::MessageRelayed { .. }
353 | Event::UserBlocked { .. }
354 | Event::UserUnblocked { .. } => (),
355 }
356 }
357
358 #[allow(dead_code)]
361 fn mls_lifecycle_exhaustiveness_ward(e: &MlsLifecycleEvent) {
362 match e {
363 MlsLifecycleEvent::Initialized { .. }
364 | MlsLifecycleEvent::EncryptionUsed { .. }
365 | MlsLifecycleEvent::DecryptionFailed { .. }
366 | MlsLifecycleEvent::SessionMissing { .. }
367 | MlsLifecycleEvent::SessionReady { .. } => (),
368 }
369 }
370
371 fn event_exemplars() -> Vec<Event> {
375 vec![
376 Event::MessageSent {
377 message_id: String::new(),
378 sender: String::new(),
379 recipient: String::new(),
380 content: String::new(),
381 priority: String::new(),
382 requires_ack: false,
383 timestamp: 0,
384 lamport_clock: 0,
385 forward_info: None,
386 },
387 Event::MessageReceived {
388 message_id: String::new(),
389 sender: String::new(),
390 recipient: String::new(),
391 content: String::new(),
392 hop_count: 0,
393 transport: String::new(),
394 timestamp: 0,
395 lamport_clock: 0,
396 reply_to_msg: None,
397 reply_context: None,
398 content_type: String::new(),
399 media_metadata: None,
400 forward_info: None,
401 encrypted: false,
402 },
403 Event::MessageDelivered {
404 message_id: String::new(),
405 latency_ms: 0,
406 hop_count: 0,
407 transport: String::new(),
408 },
409 Event::MessageFailed {
410 message_id: String::new(),
411 reason: String::new(),
412 retry_count: 0,
413 },
414 Event::MessageDecryptionFailed {
415 message_id: String::new(),
416 sender: String::new(),
417 code: crate::events::DecryptionFailureCode::Unknown,
418 reason: String::new(),
419 },
420 Event::TransportSwitched {
421 from: None,
422 to: String::new(),
423 reason: String::new(),
424 },
425 Event::RelayPromoted {
426 connection_count: 0,
427 battery_level: 0,
428 },
429 Event::RelayDemoted {
430 reason: String::new(),
431 },
432 Event::NeighborDiscovered {
433 peer_id: String::new(),
434 transport: String::new(),
435 rssi: None,
436 },
437 Event::NeighborLost {
438 peer_id: String::new(),
439 },
440 Event::IdentityReady {
441 address: String::new(),
442 },
443 Event::NetworkMetrics {
444 neighbor_count: 0,
445 relay_count: 0,
446 delivery_ratio: 0.0,
447 avg_latency_ms: 0,
448 },
449 Event::FileProgress {
450 file_id: String::new(),
451 chunks_sent: 0,
452 total_chunks: 0,
453 percentage: 0,
454 },
455 Event::FileReceived {
456 file_id: String::new(),
457 file_name: String::new(),
458 file_size: 0,
459 sender: String::new(),
460 content_type: String::new(),
461 media_metadata: None,
462 file_data: String::new(),
463 timestamp: None,
464 caption: None,
465 reply_to_msg: None,
466 reply_context: None,
467 forward_info: None,
468 },
469 Event::FileReceiveFailed {
470 file_id: String::new(),
471 file_name: String::new(),
472 sender: String::new(),
473 reason: String::new(),
474 },
475 Event::MediaSent {
476 file_id: String::new(),
477 content_type: String::new(),
478 recipient: String::new(),
479 },
480 Event::MediaSendFailed {
481 file_id: String::new(),
482 recipient: String::new(),
483 reason: String::new(),
484 },
485 Event::MessageDeferred {
486 message_id: String::new(),
487 recipient: String::new(),
488 reason: String::new(),
489 retry_count: 0,
490 next_retry_at: None,
491 },
492 Event::MessageRetrying {
493 message_id: String::new(),
494 recipient: String::new(),
495 retry_count: 0,
496 next_retry_at: 0,
497 },
498 Event::MessageUndeliverable {
499 message_id: String::new(),
500 recipient: String::new(),
501 reason: String::new(),
502 file_id: None,
503 },
504 Event::MediaResendRequired {
505 file_id: String::new(),
506 recipient: String::new(),
507 file_name: String::new(),
508 file_size: 0,
509 },
510 Event::AckEvicted {
511 message_id: String::new(),
512 priority: String::new(),
513 reason: String::new(),
514 },
515 Event::FragmentAssemblyEvicted {
516 message_id: String::new(),
517 completion_percent: 0,
518 reason: String::new(),
519 },
520 Event::RelayDemotedBattery {
521 battery_level: 0,
522 min_required: 0,
523 },
524 Event::SecureSessionEstablished {
525 peer_id: String::new(),
526 group_id: String::new(),
527 is_session: false,
528 initiated_by_local: false,
529 },
530 Event::SecureSessionFailed {
531 peer_id: String::new(),
532 reason: String::new(),
533 },
534 Event::ConvergenceDiag {
535 stage: String::new(),
536 peer_id: String::new(),
537 detail: String::new(),
538 },
539 Event::WelcomeSendAttempted {
540 peer_id: String::new(),
541 message_id: String::new(),
542 group_id: String::new(),
543 attempt: 0,
544 },
545 Event::WelcomeSendSucceeded {
546 peer_id: String::new(),
547 message_id: String::new(),
548 group_id: String::new(),
549 attempt: 0,
550 },
551 Event::WelcomeSendFailed {
552 peer_id: String::new(),
553 message_id: String::new(),
554 group_id: String::new(),
555 attempt: 0,
556 reason_code: WelcomeReasonCode::InternalError,
557 transport_error: None,
558 retryable: false,
559 next_retry_at: None,
560 },
561 Event::WelcomeSendExpired {
562 peer_id: String::new(),
563 message_id: String::new(),
564 attempt: 0,
565 reason_code: WelcomeReasonCode::RetryExhausted,
566 },
567 Event::ConnectionRequestReceived {
568 sender: String::new(),
569 sender_name: String::new(),
570 timestamp: 0,
571 key_package: None,
572 initial_message: None,
573 },
574 Event::ConnectionRequestUndeliverable {
575 recipient: String::new(),
576 message_id: String::new(),
577 reason: String::new(),
578 },
579 Event::ConnectionAccepted {
580 accepted_by: String::new(),
581 accepted_by_name: String::new(),
582 timestamp: 0,
583 key_package: None,
584 },
585 Event::ConnectionRejected {
586 rejected_by: String::new(),
587 },
588 Event::ConnectionRequestCancelled {
589 cancelled_by: String::new(),
590 },
591 Event::GroupCreated {
592 group_id: String::new(),
593 name: String::new(),
594 },
595 Event::GroupMessageReceived {
596 group_id: String::new(),
597 sender: String::new(),
598 content: String::new(),
599 timestamp: String::new(),
600 message_id: String::new(),
601 reply_to_msg: None,
602 forward_info: None,
603 media_metadata: None,
604 content_type: None,
605 },
606 Event::GroupMemberAdded {
607 group_id: String::new(),
608 user_id: String::new(),
609 added_by: String::new(),
610 group_name: None,
611 authorized: Some(true),
612 },
613 Event::GroupMemberRemoved {
614 group_id: String::new(),
615 user_id: String::new(),
616 removed_by: String::new(),
617 authorized: Some(true),
618 },
619 Event::GroupInfo {
620 group_id: String::new(),
621 name: String::new(),
622 created_by: String::new(),
623 created_at: String::new(),
624 members: Vec::new(),
625 },
626 Event::UserGroups { groups: Vec::new() },
627 Event::GroupError {
628 reason: String::new(),
629 group_id: None,
630 },
631 Event::GroupRelaySyncChanged {
632 group_id: String::new(),
633 synced: false,
634 reason: String::new(),
635 },
636 Event::GroupMessageSent {
637 group_id: String::new(),
638 message_ids: Vec::new(),
639 member_count: 0,
640 },
641 Event::GroupMessagePartialFailure {
642 group_id: String::new(),
643 failed_members: Vec::new(),
644 succeeded_members: Vec::new(),
645 },
646 Event::GroupMessageDeliveryReport {
647 group_id: String::new(),
648 message_id: String::new(),
649 delivered: Vec::new(),
650 pushed: Vec::new(),
651 missed_reissued: Vec::new(),
652 },
653 Event::GroupRichExtrasDropped {
654 group_id: String::new(),
655 unknown_members: Vec::new(),
656 },
657 Event::GroupUnauthorizedMembershipChange {
658 group_id: String::new(),
659 committer: String::new(),
660 added: Vec::new(),
661 removed: Vec::new(),
662 reason: String::new(),
663 enforced: false,
664 },
665 Event::GroupEpochForkDetected {
666 group_id: String::new(),
667 local_epoch: None,
668 },
669 Event::GroupEpochForkResolved {
670 group_id: String::new(),
671 resolved_epoch: 0,
672 failed_members: Vec::new(),
673 },
674 Event::GroupRoleChanged {
675 group_id: String::new(),
676 user_id: String::new(),
677 new_role: String::new(),
678 changed_by: String::new(),
679 },
680 Event::GroupRenamed {
681 group_id: String::new(),
682 new_name: String::new(),
683 old_name: None,
684 renamed_by: String::new(),
685 },
686 Event::ServiceDiscovered {
687 query_id: String::new(),
688 service_id: String::new(),
689 version: String::new(),
690 provider_peer_id: String::new(),
691 capabilities: HashMap::new(),
692 hop_count: 0,
693 },
694 Event::ServiceRequestReceived {
695 request_id: String::new(),
696 service_id: String::new(),
697 method: String::new(),
698 body: String::new(),
699 sender: String::new(),
700 },
701 Event::ServiceResponseReceived {
702 request_id: String::new(),
703 service_id: String::new(),
704 status: String::new(),
705 body: String::new(),
706 provider_peer_id: String::new(),
707 },
708 Event::PresenceUpdated {
709 peer_id: String::new(),
710 status: PresenceStatus::Online,
711 timestamp: 0,
712 last_seen_ms: None,
713 source: PresenceSource::Internet,
714 },
715 Event::TypingIndicatorReceived {
716 sender: String::new(),
717 conversation_id: String::new(),
718 is_typing: false,
719 timestamp: 0,
720 },
721 Event::ReadReceiptReceived {
722 sender: String::new(),
723 message_ids: Vec::new(),
724 timestamp: 0,
725 },
726 Event::DorsScoreUpdated { scores: Vec::new() },
727 Event::DorsTransportSelected {
728 from: None,
729 transport: String::new(),
730 reason_code: DorsReasonCode::InitialSelection,
731 score: 0.0,
732 },
733 Event::DorsTransportSwitched {
734 from: None,
735 to: String::new(),
736 reason_code: DorsReasonCode::PrimarySelected,
737 reason_detail: None,
738 },
739 Event::DorsEscalationTriggered {
740 phase: DorsEscalationPhase::Triggered,
741 from: String::new(),
742 to: String::new(),
743 reason_code: DorsEscalationReasonCode::FallbackSuccess,
744 reason_detail: None,
745 },
746 Event::SecurityWarning {
747 peer_id: String::new(),
748 reason_code: SecurityWarningCode::SenderAddressMismatch,
749 reason: String::new(),
750 },
751 Event::MessageRelayed {
752 message_id: String::new(),
753 sender: String::new(),
754 recipient: String::new(),
755 hop_count: 0,
756 remaining_ttl: 0,
757 },
758 Event::UserBlocked {
759 user_id: String::new(),
760 },
761 Event::UserUnblocked {
762 user_id: String::new(),
763 },
764 ]
765 }
766
767 fn mls_lifecycle_exemplars() -> Vec<MlsLifecycleEvent> {
770 vec![
771 MlsLifecycleEvent::Initialized {
772 timestamp_ms: 0,
773 session_id: String::new(),
774 group_id: None,
775 peer_id: None,
776 context: MlsOperationContext::Initialize,
777 error_category: None,
778 },
779 MlsLifecycleEvent::EncryptionUsed {
780 timestamp_ms: 0,
781 session_id: String::new(),
782 group_id: None,
783 peer_id: None,
784 context: MlsOperationContext::Send,
785 error_category: None,
786 },
787 MlsLifecycleEvent::DecryptionFailed {
788 timestamp_ms: 0,
789 session_id: String::new(),
790 group_id: None,
791 peer_id: None,
792 context: MlsOperationContext::Receive,
793 error_category: None,
794 failure_kind: DecryptionFailureKind::Unknown,
795 },
796 MlsLifecycleEvent::SessionMissing {
797 timestamp_ms: 0,
798 session_id: String::new(),
799 group_id: None,
800 peer_id: None,
801 context: MlsOperationContext::SessionLookup,
802 error_category: None,
803 },
804 MlsLifecycleEvent::SessionReady {
805 timestamp_ms: 0,
806 session_id: String::new(),
807 group_id: None,
808 peer_id: None,
809 context: MlsOperationContext::Welcome,
810 error_category: None,
811 },
812 ]
813 }
814
815 fn simple_record_names() -> Vec<&'static str> {
819 use crate::telemetry::device::DeviceCapabilitySnapshot;
820 use crate::telemetry::metrics_snapshot::MetricsFrame;
821 use crate::telemetry::routing::RoutingDecision;
822 use crate::telemetry::transport_state::TransportStateEvent;
823 vec![
824 MetricsFrame::NAME,
825 TransportStateEvent::NAME,
826 RoutingDecision::NAME,
827 DeviceCapabilitySnapshot::NAME,
828 ]
829 }
830
831 #[test]
839 fn impl_telemetry_names_are_unique() {
840 let mut names: Vec<&'static str> = Vec::new();
841 for e in event_exemplars() {
842 names.push(e.telemetry_name());
843 }
844 for e in mls_lifecycle_exemplars() {
845 names.push(e.telemetry_name());
846 }
847 names.extend(simple_record_names());
848 let unique: HashSet<&&'static str> = names.iter().collect();
849 assert_eq!(
850 unique.len(),
851 names.len(),
852 "two variants produced the same telemetry_name() — distinct \
853 events would silently merge at the sink. Names: {names:?}",
854 );
855 }
856
857 #[test]
864 fn impl_telemetry_names_match_catalogue() {
865 let mut impl_names: Vec<&'static str> = Vec::new();
866 for e in event_exemplars() {
867 impl_names.push(e.telemetry_name());
868 }
869 for e in mls_lifecycle_exemplars() {
870 impl_names.push(e.telemetry_name());
871 }
872 impl_names.extend(simple_record_names());
873
874 let impl_set: HashSet<&&'static str> = impl_names.iter().collect();
875 let catalogue_set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
876
877 let missing_from_catalogue: Vec<&&'static str> = impl_set
878 .iter()
879 .copied()
880 .filter(|n| !ALL_TELEMETRY_NAMES.contains(*n))
881 .collect();
882 assert!(
883 missing_from_catalogue.is_empty(),
884 "implementation produces names not in ALL_TELEMETRY_NAMES: \
885 {missing_from_catalogue:?}. Update the catalogue.",
886 );
887
888 let stale_in_catalogue: Vec<&&str> = catalogue_set
889 .iter()
890 .copied()
891 .filter(|n| !impl_names.iter().any(|produced| produced == *n))
892 .collect();
893 assert!(
894 stale_in_catalogue.is_empty(),
895 "catalogue carries names the implementation does not produce: \
896 {stale_in_catalogue:?}. Remove from the catalogue or restore \
897 the match arm.",
898 );
899
900 assert_eq!(
901 impl_names.len(),
902 ALL_TELEMETRY_NAMES.len(),
903 "catalogue length ({}) differs from implementation variant \
904 count ({})",
905 ALL_TELEMETRY_NAMES.len(),
906 impl_names.len(),
907 );
908 }
909
910 #[test]
916 fn catalogue_entries_are_unique() {
917 let set: HashSet<&&str> = ALL_TELEMETRY_NAMES.iter().collect();
918 assert_eq!(
919 set.len(),
920 ALL_TELEMETRY_NAMES.len(),
921 "duplicate entry in ALL_TELEMETRY_NAMES",
922 );
923 }
924
925 #[test]
926 fn all_telemetry_names_match_grammar() {
927 for name in ALL_TELEMETRY_NAMES {
928 assert!(
929 is_well_formed(name),
930 "telemetry name '{name}' does not match snake.dot.case grammar",
931 );
932 }
933 }
934
935 #[test]
936 fn grammar_checker_rejects_malformed_names() {
937 assert!(!is_well_formed(""));
938 assert!(!is_well_formed("flat"));
939 assert!(!is_well_formed(".leading"));
940 assert!(!is_well_formed("trailing."));
941 assert!(!is_well_formed("double..dot"));
942 assert!(!is_well_formed("Upper.Case"));
943 assert!(!is_well_formed("has-dash.segment"));
944 assert!(is_well_formed("ok.name"));
945 assert!(is_well_formed("a.b.c"));
946 assert!(is_well_formed("with_underscore.segment"));
947 assert!(is_well_formed("has_digit1.seg2"));
948 }
949}