exception_collector/
dedup.rs1use std::sync::Arc;
8
9use crate::{ExceptionBuffer, ExceptionRecord};
10
11#[derive(Debug)]
18pub struct DedupEngine {
19 buffer: Arc<ExceptionBuffer>,
20}
21
22impl DedupEngine {
23 #[must_use]
25 pub fn new(buffer: Arc<ExceptionBuffer>) -> Self {
26 Self { buffer }
27 }
28
29 #[must_use]
31 pub fn is_duplicate(&self, signature: &str) -> bool {
32 self.buffer.contains_signature(signature)
33 }
34
35 #[must_use]
37 pub fn distinct_count(&self) -> usize {
38 self.buffer.len()
39 }
40
41 #[must_use]
43 pub fn pending_batch(&self) -> Vec<ExceptionRecord> {
44 self.buffer.unreported_samples()
45 }
46}
47
48#[cfg(test)]
49#[allow(
50 clippy::unwrap_used,
51 clippy::unwrap_in_result,
52 clippy::expect_used,
53 clippy::panic,
54 clippy::pedantic,
55 clippy::disallowed_methods,
56 clippy::indexing_slicing,
57 reason = "test module relaxes production lint strictness"
58)]
59mod tests {
60 use super::*;
61 use crate::{ExceptionKind, ExceptionRecord};
62
63 struct TestCtx {
64 buffer: Arc<ExceptionBuffer>,
65 #[allow(dead_code, reason = "field keeps TempDir alive for test scope")]
66 _dir: tempfile::TempDir,
67 }
68
69 fn make_ctx() -> TestCtx {
70 let dir = tempfile::tempdir().unwrap();
71 let db_path = dir.path().join("test.db");
72 TestCtx {
73 buffer: Arc::new(ExceptionBuffer::new(&db_path).unwrap()),
74 _dir: dir,
75 }
76 }
77
78 fn make_record(component: &str, msg: &str) -> ExceptionRecord {
79 ExceptionRecord::new(component, ExceptionKind::Panic, msg, "frame1\nframe2")
80 }
81
82 #[test]
83 fn test_should_detect_duplicate_by_signature() {
84 let ctx = make_ctx();
85 let engine = DedupEngine::new(ctx.buffer.clone());
86
87 let record = make_record("comp-a", "same error");
88 ctx.buffer.collect(record);
89
90 let sig = crate::compute_signature(&make_record("comp-a", "same error"));
91 assert!(engine.is_duplicate(&sig));
92 }
93
94 #[test]
95 fn test_should_not_detect_nonexistent_signature() {
96 let ctx = make_ctx();
97 let engine = DedupEngine::new(ctx.buffer);
98
99 assert!(!engine.is_duplicate("nonexistent_signature_here"));
100 }
101
102 #[test]
103 fn test_should_count_distinct_signatures() {
104 let ctx = make_ctx();
105 let engine = DedupEngine::new(ctx.buffer.clone());
106
107 ctx.buffer.collect(make_record("comp-a", "error one"));
108 ctx.buffer.collect(make_record("comp-b", "error two"));
109 ctx.buffer.collect(make_record("comp-a", "error one")); assert_eq!(engine.distinct_count(), 2);
112 }
113
114 #[test]
115 fn test_should_count_zero_when_empty() {
116 let ctx = make_ctx();
117 let engine = DedupEngine::new(ctx.buffer);
118
119 assert_eq!(engine.distinct_count(), 0);
120 }
121
122 #[test]
123 fn test_should_return_correct_pending_batch() {
124 let ctx = make_ctx();
125 let engine = DedupEngine::new(ctx.buffer.clone());
126
127 ctx.buffer.collect(make_record("comp-a", "error one"));
128 ctx.buffer.collect(make_record("comp-b", "error two"));
129
130 let pending = engine.pending_batch();
131 assert_eq!(pending.len(), 2);
132 }
133
134 #[test]
135 fn test_should_not_include_reported_in_pending_batch() {
136 let ctx = make_ctx();
137 let engine = DedupEngine::new(ctx.buffer.clone());
138
139 let r1 = make_record("comp-a", "error one");
140 let r2 = make_record("comp-b", "error two");
141 ctx.buffer.collect(r1.clone());
142 ctx.buffer.collect(r2.clone());
143 ctx.buffer.flush().unwrap();
144
145 let sig1 = crate::compute_signature(&r1);
146 ctx.buffer
147 .mark_reported(std::slice::from_ref(&sig1))
148 .unwrap();
149
150 let pending = engine.pending_batch();
151 assert_eq!(pending.len(), 1);
152 assert_eq!(pending[0].component, "comp-b");
153 }
154
155 #[test]
156 fn test_should_return_empty_pending_when_all_reported() {
157 let ctx = make_ctx();
158 let engine = DedupEngine::new(ctx.buffer.clone());
159
160 let r = make_record("comp", "reported");
161 ctx.buffer.collect(r.clone());
162 ctx.buffer.flush().unwrap();
163
164 let sig = crate::compute_signature(&r);
165 ctx.buffer
166 .mark_reported(std::slice::from_ref(&sig))
167 .unwrap();
168
169 let pending = engine.pending_batch();
170 assert!(pending.is_empty());
171 }
172
173 #[test]
174 fn test_should_return_empty_pending_when_buffer_empty() {
175 let ctx = make_ctx();
176 let engine = DedupEngine::new(ctx.buffer);
177
178 let pending = engine.pending_batch();
179 assert!(pending.is_empty());
180 }
181}