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

gripmock / grpctestify-rust / 29118827835

10 Jul 2026 07:40PM UTC coverage: 73.631% (-0.02%) from 73.652%
29118827835

Pull #52

github

web-flow
Merge 1bc2bee86 into 000de5abf
Pull Request #52: 2q, indexed fix

46 of 84 new or added lines in 22 files covered. (54.76%)

16 existing lines in 5 files now uncovered.

29168 of 39614 relevant lines covered (73.63%)

51333.98 hits per line

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

46.28
/src/execution/request_handler.rs
1
// Request Handler - handles request building and sending
2

3
#[cfg(test)]
4
use crate::grpc::GrpcClientConfig;
5
#[cfg(test)]
6
use crate::parser::ast::SectionType;
7
use crate::parser::ast::{Section, SectionContent};
8
use crate::report::CoverageCollector;
9
#[cfg(test)]
10
use crate::utils::file::FileUtils;
11
use prost_reflect::MessageDescriptor;
12
use serde_json::Value;
13
#[cfg(test)]
14
use std::path::Path;
15
use std::sync::Arc;
16
use tokio::sync::mpsc::Sender;
17

18
/// Request building and sending result
19
#[derive(Debug, Clone)]
20
pub struct RequestSendResult {
21
    pub success: bool,
22
    pub error_message: Option<String>,
23
}
24

25
/// Request Handler - builds and sends requests
26
pub struct RequestHandler {
27
    coverage_collector: Option<Arc<CoverageCollector>>,
28
}
29

30
impl RequestHandler {
31
    /// Create new request handler
32
    pub fn new(
56✔
33
        _no_assert: bool,
56✔
34
        _verbose: bool,
56✔
35
        coverage_collector: Option<Arc<CoverageCollector>>,
56✔
36
    ) -> Self {
56✔
37
        Self { coverage_collector }
56✔
38
    }
56✔
39

40
    /// Build request value from section
41
    pub fn build_request(
4✔
42
        &self,
4✔
43
        section: &Section,
4✔
44
        variables: &std::collections::HashMap<String, Value>,
4✔
45
    ) -> Option<Value> {
4✔
46
        match &section.content {
4✔
47
            SectionContent::Json(value) => {
4✔
48
                let mut request = value.clone();
4✔
49
                self.substitute_variables(&mut request, variables);
4✔
50
                Some(request)
4✔
51
            }
52
            SectionContent::JsonLines(_) => {
53
                // For JSON lines, each line is a separate request
54
                // This is handled by send_requests_batch
55
                None
×
56
            }
57
            _ => None,
×
58
        }
59
    }
4✔
60

61
    /// Send a single request
62
    pub async fn send_request(
×
63
        &self,
×
64
        tx: &Sender<Value>,
×
65
        request_value: Value,
×
66
        section_line: usize,
×
67
        _msg_type: Option<&MessageDescriptor>,
×
68
    ) -> RequestSendResult {
×
69
        // Coverage: record request fields (simplified - would need message type name)
70
        if let Some(_collector) = &self.coverage_collector {
×
71
            // collector.record_fields_from_json(msg_type_name, &request_value);
×
72
        }
×
73

74
        match tx.send(request_value).await {
×
75
            Ok(_) => RequestSendResult {
×
76
                success: true,
×
77
                error_message: None,
×
78
            },
×
79
            Err(e) => RequestSendResult {
×
80
                success: false,
×
81
                error_message: Some(format!(
×
82
                    "Failed to send request at line {}: {}",
×
83
                    section_line, e
×
84
                )),
×
85
            },
×
86
        }
87
    }
×
88

89
    /// Send implicit empty request (for unary/server-stream when no REQUEST section)
90
    pub async fn send_implicit_empty_request(&self, tx: &Sender<Value>) -> RequestSendResult {
×
91
        let empty_request = Value::Object(serde_json::Map::new());
×
92

93
        match tx.send(empty_request).await {
×
94
            Ok(_) => RequestSendResult {
×
95
                success: true,
×
96
                error_message: None,
×
97
            },
×
98
            Err(e) => RequestSendResult {
×
99
                success: false,
×
100
                error_message: Some(format!("Failed to send implicit empty request: {}", e)),
×
101
            },
×
102
        }
103
    }
×
104

105
    /// Check if request stream should be closed (test-only)
106
    #[cfg(test)]
107
    pub fn should_close_request_stream(&self, sections: &[Section], current_index: usize) -> bool {
2✔
108
        // Close stream if no more REQUEST sections follow
109
        sections[current_index + 1..]
2✔
110
            .iter()
2✔
111
            .all(|s| s.section_type != SectionType::Request)
2✔
112
    }
2✔
113

114
    /// Substitute variables in request value
115
    pub fn substitute_variables(
14✔
116
        &self,
14✔
117
        value: &mut Value,
14✔
118
        variables: &std::collections::HashMap<String, Value>,
14✔
119
    ) {
14✔
120
        match value {
14✔
121
            Value::String(s) => {
6✔
122
                for (var_name, var_value) in variables {
6✔
123
                    let pattern = format!("{{{{ {} }}}}", var_name);
6✔
124
                    if s.contains(&pattern) {
6✔
125
                        if let Value::String(replacement) = var_value {
4✔
126
                            *s = s.replace(&pattern, replacement);
4✔
127
                        } else {
4✔
128
                            *s = s.replace(&pattern, &var_value.to_string());
×
129
                        }
×
130
                    }
2✔
131
                }
132
            }
133
            Value::Array(arr) => {
×
134
                for item in arr {
×
135
                    self.substitute_variables(item, variables);
×
136
                }
×
137
            }
138
            Value::Object(map) => {
6✔
139
                for (_, val) in map {
8✔
140
                    self.substitute_variables(val, variables);
8✔
141
                }
8✔
142
            }
143
            _ => {}
2✔
144
        }
145
    }
14✔
146

147
    /// Build TLS config from document (test-only)
148
    #[cfg(test)]
149
    pub fn build_tls_config(
×
150
        document: &crate::parser::ast::GctfDocument,
×
151
        document_path: &Path,
×
152
    ) -> Option<crate::grpc::TlsConfig> {
×
153
        document
×
154
            .get_tls_config()
×
155
            .map(|tls_map| crate::grpc::TlsConfig {
×
156
                ca_cert_path: tls_map.get("ca_cert").map(|p| {
×
157
                    FileUtils::resolve_relative_path(document_path, p)
×
158
                        .to_string_lossy()
×
159
                        .to_string()
×
160
                }),
×
161
                client_cert_path: tls_map.get("client_cert").map(|p| {
×
162
                    FileUtils::resolve_relative_path(document_path, p)
×
163
                        .to_string_lossy()
×
164
                        .to_string()
×
165
                }),
×
166
                client_key_path: tls_map.get("client_key").map(|p| {
×
167
                    FileUtils::resolve_relative_path(document_path, p)
×
168
                        .to_string_lossy()
×
169
                        .to_string()
×
170
                }),
×
171
                server_name: tls_map.get("server_name").cloned(),
×
172
                insecure_skip_verify: tls_map
×
173
                    .get("insecure")
×
NEW
174
                    .is_some_and(|v| v == "true" || v == "1"),
×
175
            })
×
176
    }
×
177

178
    /// Build proto config from document (test-only)
179
    #[cfg(test)]
180
    pub fn build_proto_config(
×
181
        document: &crate::parser::ast::GctfDocument,
×
182
        document_path: &Path,
×
183
    ) -> Option<crate::grpc::ProtoConfig> {
×
184
        document.get_proto_config().map(|proto_map| {
×
185
            let files = proto_map
×
186
                .get("files")
×
187
                .map(|f| {
×
188
                    f.split(',')
×
189
                        .map(|s| {
×
190
                            FileUtils::resolve_relative_path(document_path, s.trim())
×
191
                                .to_string_lossy()
×
192
                                .to_string()
×
193
                        })
×
194
                        .collect()
×
195
                })
×
196
                .unwrap_or_default();
×
197

198
            let import_paths = proto_map
×
199
                .get("import_paths")
×
200
                .map(|p| {
×
201
                    p.split(',')
×
202
                        .map(|s| {
×
203
                            FileUtils::resolve_relative_path(document_path, s.trim())
×
204
                                .to_string_lossy()
×
205
                                .to_string()
×
206
                        })
×
207
                        .collect()
×
208
                })
×
209
                .unwrap_or_default();
×
210

211
            let descriptor = proto_map.get("descriptor").map(|p| {
×
212
                FileUtils::resolve_relative_path(document_path, p)
×
213
                    .to_string_lossy()
×
214
                    .to_string()
×
215
            });
×
216

217
            crate::grpc::ProtoConfig {
×
218
                files,
×
219
                import_paths,
×
220
                descriptor,
×
221
            }
×
222
        })
×
223
    }
×
224

225
    /// Build gRPC client config (test-only)
226
    #[cfg(test)]
227
    pub fn build_client_config(
×
228
        document: &crate::parser::ast::GctfDocument,
×
229
        document_path: &Path,
×
230
        address: &str,
×
231
    ) -> GrpcClientConfig {
×
232
        let tls_config = Self::build_tls_config(document, document_path);
×
233
        let proto_config = Self::build_proto_config(document, document_path);
×
234

235
        GrpcClientConfig {
236
            address: address.to_string(),
×
237
            timeout_seconds: 30, // Default timeout
238
            tls_config,
×
239
            proto_config,
×
240
            metadata: document.get_request_headers(),
×
241
            compression: crate::grpc::CompressionMode::from_env(),
×
242
            target_service: document.parse_endpoint().map(|(p, s, m)| {
×
243
                if p.is_empty() {
×
244
                    format!("{}/{}", s, m)
×
245
                } else {
246
                    format!("{}.{}/{}", p, s, m)
×
247
                }
248
            }),
×
249
        }
250
    }
×
251
}
252

253
#[cfg(test)]
254
mod tests {
255
    use super::*;
256
    use serde_json::json;
257

258
    #[test]
259
    fn test_request_handler_new() {
2✔
260
        let handler = RequestHandler::new(false, false, None);
2✔
261
        assert!(handler.coverage_collector.is_none());
2✔
262
    }
2✔
263

264
    #[test]
265
    fn test_build_request_json() {
2✔
266
        let handler = RequestHandler::new(false, false, None);
2✔
267
        let section = Section {
2✔
268
            section_type: SectionType::Request,
2✔
269
            content: SectionContent::Json(json!({"id": 123})),
2✔
270
            inline_options: Default::default(),
2✔
271
            raw_content: "".to_string(),
2✔
272
            start_line: 0,
2✔
273
            end_line: 0,
2✔
274
            attributes: Vec::new(),
2✔
275
        };
2✔
276
        let variables = std::collections::HashMap::new();
2✔
277

278
        let result = handler.build_request(&section, &variables);
2✔
279
        assert!(result.is_some());
2✔
280
        assert_eq!(result.unwrap(), json!({"id": 123}));
2✔
281
    }
2✔
282

283
    #[test]
284
    fn test_substitute_variables() {
2✔
285
        let handler = RequestHandler::new(false, false, None);
2✔
286
        let mut value = json!({"id": "{{ user_id }}", "name": "test"});
2✔
287
        let mut variables = std::collections::HashMap::new();
2✔
288
        variables.insert("user_id".to_string(), json!("123"));
2✔
289

290
        handler.substitute_variables(&mut value, &variables);
2✔
291

292
        assert_eq!(value["id"], "123");
2✔
293
        assert_eq!(value["name"], "test");
2✔
294
    }
2✔
295

296
    #[test]
297
    fn test_should_close_request_stream() {
2✔
298
        let handler = RequestHandler::new(false, false, None);
2✔
299
        let sections = vec![
2✔
300
            Section {
2✔
301
                section_type: SectionType::Request,
2✔
302
                content: SectionContent::Json(json!({})),
2✔
303
                inline_options: Default::default(),
2✔
304
                raw_content: "".to_string(),
2✔
305
                start_line: 0,
2✔
306
                end_line: 0,
2✔
307
                attributes: Vec::new(),
2✔
308
            },
2✔
309
            Section {
2✔
310
                section_type: SectionType::Response,
2✔
311
                content: SectionContent::Json(json!({})),
2✔
312
                inline_options: Default::default(),
2✔
313
                raw_content: "".to_string(),
2✔
314
                start_line: 0,
2✔
315
                end_line: 0,
2✔
316
                attributes: Vec::new(),
2✔
317
            },
2✔
318
        ];
319

320
        // After first section (index 0), there are no more REQUEST sections
321
        assert!(handler.should_close_request_stream(&sections, 0));
2✔
322
    }
2✔
323

324
    #[test]
325
    fn test_build_request_with_variables() {
2✔
326
        let handler = RequestHandler::new(false, false, None);
2✔
327
        let section = Section {
2✔
328
            section_type: SectionType::Request,
2✔
329
            content: SectionContent::Json(json!({"id": "{{ user_id }}"})),
2✔
330
            inline_options: Default::default(),
2✔
331
            raw_content: "".to_string(),
2✔
332
            start_line: 0,
2✔
333
            end_line: 0,
2✔
334
            attributes: Vec::new(),
2✔
335
        };
2✔
336
        let mut variables = std::collections::HashMap::new();
2✔
337
        variables.insert("user_id".to_string(), json!("456"));
2✔
338

339
        let result = handler.build_request(&section, &variables);
2✔
340
        assert!(result.is_some());
2✔
341
        assert_eq!(result.unwrap()["id"], "456");
2✔
342
    }
2✔
343
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc