• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24368153097

13 Apr 2026 09:36PM UTC coverage: 75.096% (-0.3%) from 75.445%
24368153097

Pull #35

github

web-flow
Merge 97a02fd78 into 4ba0f08f1
Pull Request #35: feat: meta section & refactoring

2518 of 3592 new or added lines in 47 files covered. (70.1%)

155 existing lines in 9 files now uncovered.

16781 of 22346 relevant lines covered (75.1%)

2495.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/report/coverage.rs
1
//! gRPC method and protobuf message field coverage collector.
2
//!
3
//! Tracks which gRPC service/method calls were made during test execution
4
//! and which protobuf message fields were covered by assertions.
5

6
use prost_reflect::{DescriptorPool, MessageDescriptor};
7
use serde::{Deserialize, Serialize};
8
use std::collections::{HashMap, HashSet};
9
use std::sync::{Arc, Mutex};
10

11
/// Coverage data for a single file.
12
#[derive(Debug, Clone, Serialize, Deserialize)]
13
pub 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
/// Coverage statistics (covered vs total).
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct CoverageStats {
27
    pub covered: usize,
28
    pub total: usize,
29
}
30

31
/// Coverage data for a protobuf message type's fields.
32
#[derive(Debug, Clone, Serialize, Deserialize)]
33
pub struct MessageFieldCoverage {
34
    pub message_type: String,
35
    pub covered_fields: Vec<String>,
36
    pub total_fields: usize,
37
}
38

39
/// Full coverage report with file and message-level statistics.
40
#[derive(Debug, Clone, Serialize, Deserialize)]
41
pub struct CoverageReport {
42
    pub files: Vec<CoverageFile>,
43
    pub messages: Vec<MessageFieldCoverage>,
44
    pub summary: CoverageStats,
45
    pub field_summary: CoverageStats,
46
}
47

48
/// Collects gRPC method call and protobuf field coverage during test execution.
49
#[derive(Debug, Clone)]
50
pub 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

56
impl 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) {
×
NEW
66
        let mut calls = self.calls.lock().expect("CoverageCollector lock poisoned");
×
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) {
×
NEW
72
        let mut fields = self
×
NEW
73
            .fields_covered
×
NEW
74
            .lock()
×
NEW
75
            .expect("CoverageCollector lock poisoned");
×
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) {
×
NEW
103
        let mut pool = self.pool.lock().expect("CoverageCollector lock poisoned");
×
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 {
×
NEW
122
        let calls = self.calls.lock().expect("CoverageCollector lock poisoned");
×
NEW
123
        let pool = self.pool.lock().expect("CoverageCollector lock poisoned");
×
NEW
124
        let fields_covered = self
×
NEW
125
            .fields_covered
×
NEW
126
            .lock()
×
NEW
127
            .expect("CoverageCollector lock poisoned");
×
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
        // Method coverage - deduplicated iteration pattern
137
        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
        // Message field coverage
170
        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 {
×
NEW
221
        let calls = self.calls.lock().expect("CoverageCollector lock poisoned");
×
NEW
222
        let pool = self.pool.lock().expect("CoverageCollector lock poisoned");
×
NEW
223
        let fields_covered = self
×
NEW
224
            .fields_covered
×
NEW
225
            .lock()
×
NEW
226
            .expect("CoverageCollector lock poisoned");
×
227

228
        let mut report = String::new();
×
229
        report.push_str("--- gRPC API Coverage Report ---\n\n");
×
230

231
        // Method coverage
232
        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
        // Message field coverage
285
        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

322
impl Default for CoverageCollector {
323
    fn default() -> Self {
×
324
        Self::new()
×
325
    }
×
326
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc