1use prost_reflect::{DescriptorPool, MessageDescriptor};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::sync::{Arc, Mutex};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CoverageFile {
14 pub uri: String,
15 pub statements: CoverageStats,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub branches: Option<CoverageStats>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub functions: Option<CoverageStats>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub fields: Option<CoverageStats>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CoverageStats {
27 pub covered: usize,
28 pub total: usize,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct MessageFieldCoverage {
34 pub message_type: String,
35 pub covered_fields: Vec<String>,
36 pub total_fields: usize,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CoverageReport {
42 pub files: Vec<CoverageFile>,
43 pub messages: Vec<MessageFieldCoverage>,
44 pub summary: CoverageStats,
45 pub field_summary: CoverageStats,
46}
47
48#[derive(Debug, Clone)]
50pub struct CoverageCollector {
51 calls: Arc<Mutex<HashMap<String, HashMap<String, u64>>>>,
52 pool: Arc<Mutex<DescriptorPool>>,
53 fields_covered: Arc<Mutex<HashMap<String, HashSet<String>>>>,
54}
55
56impl CoverageCollector {
57 pub fn new() -> Self {
58 Self {
59 calls: Arc::new(Mutex::new(HashMap::new())),
60 pool: Arc::new(Mutex::new(DescriptorPool::new())),
61 fields_covered: Arc::new(Mutex::new(HashMap::new())),
62 }
63 }
64
65 pub fn record_call(&self, service: &str, method: &str) {
66 let mut calls = self.calls.lock().unwrap_or_else(|e| e.into_inner());
67 let service_calls = calls.entry(service.to_string()).or_default();
68 *service_calls.entry(method.to_string()).or_insert(0) += 1;
69 }
70
71 pub fn record_fields_from_json(&self, message_type: &str, json: &serde_json::Value) {
72 let mut fields = self
73 .fields_covered
74 .lock()
75 .unwrap_or_else(|e| e.into_inner());
76 let message_fields = fields.entry(message_type.to_string()).or_default();
77 Self::extract_fields_from_json(json, message_fields, "");
78 }
79
80 fn extract_fields_from_json(
81 json: &serde_json::Value,
82 fields: &mut HashSet<String>,
83 prefix: &str,
84 ) {
85 if let serde_json::Value::Object(map) = json {
86 for (key, value) in map {
87 let field_path = if prefix.is_empty() {
88 key.clone()
89 } else {
90 format!("{}.{}", prefix, key)
91 };
92 fields.insert(field_path.clone());
93 Self::extract_fields_from_json(value, fields, &field_path);
94 }
95 } else if let serde_json::Value::Array(arr) = json {
96 for item in arr {
97 Self::extract_fields_from_json(item, fields, prefix);
98 }
99 }
100 }
101
102 pub fn register_pool(&self, other: &DescriptorPool) {
103 let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
104 for file in other.files() {
105 let _ = pool.add_file_descriptor_proto(file.file_descriptor_proto().clone());
106 }
107 }
108
109 fn count_message_fields(pool: &DescriptorPool, message_type: &str) -> usize {
110 if let Some(msg) = pool.get_message_by_name(message_type) {
111 Self::count_fields_recursive(&msg)
112 } else {
113 0
114 }
115 }
116
117 fn count_fields_recursive(msg: &MessageDescriptor) -> usize {
118 msg.fields().count()
119 }
120
121 pub fn generate_json_report(&self) -> CoverageReport {
122 let calls = self.calls.lock().unwrap_or_else(|e| e.into_inner());
123 let pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
124 let fields_covered = self
125 .fields_covered
126 .lock()
127 .unwrap_or_else(|e| e.into_inner());
128
129 let mut files = Vec::new();
130 let mut messages = Vec::new();
131 let mut total_covered = 0;
132 let mut total_methods = 0;
133 let mut total_fields_covered = 0;
134 let mut total_fields = 0;
135
136 let mut services: Vec<_> = pool.services().collect();
138 services.sort_by(|a, b| a.name().cmp(b.name()));
139
140 for service in services {
141 let service_name = service.name();
142 if service_name.contains("reflection") {
143 continue;
144 }
145
146 let methods: Vec<_> = service.methods().collect();
147 let called_methods = calls.get(service_name).cloned().unwrap_or_default();
148
149 let covered = methods
150 .iter()
151 .filter(|m| called_methods.get(m.name()).unwrap_or(&0) > &0)
152 .count();
153 let total = methods.len();
154
155 if total > 0 {
156 total_covered += covered;
157 total_methods += total;
158
159 files.push(CoverageFile {
160 uri: format!("grpc://{}", service_name),
161 statements: CoverageStats { covered, total },
162 branches: None,
163 functions: Some(CoverageStats { covered, total }),
164 fields: None,
165 });
166 }
167 }
168
169 let mut all_message_types: HashSet<String> = HashSet::new();
171 for message_type in fields_covered.keys() {
172 all_message_types.insert(message_type.clone());
173 }
174
175 let mut sorted_messages: Vec<_> = all_message_types.into_iter().collect();
176 sorted_messages.sort();
177
178 for message_type in sorted_messages {
179 let covered = fields_covered
180 .get(&message_type)
181 .map(|s| s.len())
182 .unwrap_or(0);
183 let total = Self::count_message_fields(&pool, &message_type);
184
185 if total > 0 {
186 total_fields_covered += covered.min(total);
187 total_fields += total;
188
189 let covered_fields: Vec<String> = fields_covered
190 .get(&message_type)
191 .map(|s| {
192 let mut v: Vec<_> = s.iter().cloned().collect();
193 v.sort();
194 v
195 })
196 .unwrap_or_default();
197
198 messages.push(MessageFieldCoverage {
199 message_type,
200 covered_fields,
201 total_fields: total,
202 });
203 }
204 }
205
206 CoverageReport {
207 files,
208 messages,
209 summary: CoverageStats {
210 covered: total_covered,
211 total: total_methods,
212 },
213 field_summary: CoverageStats {
214 covered: total_fields_covered,
215 total: total_fields,
216 },
217 }
218 }
219
220 pub fn generate_text_report(&self) -> String {
221 let calls = self.calls.lock().unwrap_or_else(|e| e.into_inner());
222 let pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
223 let fields_covered = self
224 .fields_covered
225 .lock()
226 .unwrap_or_else(|e| e.into_inner());
227
228 let mut report = String::new();
229 report.push_str("--- gRPC API Coverage Report ---\n\n");
230
231 let mut services: Vec<_> = pool.services().collect();
233 services.sort_by(|a, b| a.name().cmp(b.name()));
234
235 if services.is_empty() {
236 report.push_str("No services found in descriptors.\n");
237 return report;
238 }
239
240 for service in services {
241 let service_name = service.name();
242 if service_name == "grpc.reflection.v1alpha.ServerReflection"
243 || service_name == "grpc.reflection.v1.ServerReflection"
244 {
245 continue;
246 }
247
248 report.push_str(&format!("Service: {}\n", service_name));
249
250 let called_methods = calls.get(service_name).cloned().unwrap_or_default();
251
252 let mut methods: Vec<_> = service.methods().collect();
253 methods.sort_by(|a, b| a.name().cmp(b.name()));
254
255 let mut covered_count = 0;
256 let total_count = methods.len();
257
258 for method in methods {
259 let method_name = method.name();
260 let count = called_methods.get(method_name).unwrap_or(&0);
261
262 let status = if *count > 0 {
263 covered_count += 1;
264 format!("✅ ({} calls)", count)
265 } else {
266 "❌ (0 calls)".to_string()
267 };
268
269 report.push_str(&format!(" - {}: {}\n", method_name, status));
270 }
271
272 let coverage_pct = if total_count > 0 {
273 (covered_count as f64 / total_count as f64) * 100.0
274 } else {
275 0.0
276 };
277
278 report.push_str(&format!(
279 " Coverage: {:.1}% ({}/{})\n\n",
280 coverage_pct, covered_count, total_count
281 ));
282 }
283
284 if !fields_covered.is_empty() {
286 report.push_str("--- Message Field Coverage ---\n\n");
287
288 let mut message_types: Vec<_> = fields_covered.keys().cloned().collect();
289 message_types.sort();
290
291 for message_type in message_types {
292 let covered = fields_covered
293 .get(&message_type)
294 .map(|s| s.len())
295 .unwrap_or(0);
296 let total = Self::count_message_fields(&pool, &message_type);
297
298 if total > 0 {
299 let pct = (covered.min(total) as f64 / total as f64) * 100.0;
300 let status = if pct >= 100.0 {
301 "✅"
302 } else if pct > 0.0 {
303 "⚠️"
304 } else {
305 "❌"
306 };
307 report.push_str(&format!(
308 "{} {} ({}/{})\n",
309 status,
310 message_type,
311 covered.min(total),
312 total
313 ));
314 }
315 }
316 }
317
318 report
319 }
320}
321
322impl Default for CoverageCollector {
323 fn default() -> Self {
324 Self::new()
325 }
326}