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

gripmock / grpctestify-rust / 24382834125

14 Apr 2026 05:36AM UTC coverage: 76.46% (+1.0%) from 75.445%
24382834125

Pull #35

github

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

2932 of 3905 new or added lines in 48 files covered. (75.08%)

155 existing lines in 9 files now uncovered.

17312 of 22642 relevant lines covered (76.46%)

2462.81 hits per line

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

96.21
/src/parser/content_parser.rs
1
//! Section content parser for GCTF files.
2
//!
3
//! Parses the content of different section types based on their structure.
4

5
use anyhow::Result;
6

7
use crate::parser::ast::{FileMeta, InlineOptions, Section, SectionContent, SectionType};
8
use crate::parser::gctf_tokenizer::{
9
    tokenize_extract_line, tokenize_inline_options, tokenize_kv_line,
10
};
11
use crate::parser::json_mod;
12
use crate::parser::json_stream_parser;
13

14
/// Parse section content based on section type.
15
pub fn parse_section_content(section_type: SectionType, content: &str) -> Result<SectionContent> {
132,542✔
16
    let content = content.trim();
132,542✔
17

18
    if content.is_empty() {
132,542✔
19
        return Ok(SectionContent::Empty);
2✔
20
    }
132,540✔
21

22
    match section_type {
132,540✔
23
        // Single value sections
24
        SectionType::Address | SectionType::Endpoint => {
25
            Ok(SectionContent::Single(content.to_string()))
40,534✔
26
        }
27

28
        // JSON sections
29
        SectionType::Request | SectionType::Error => {
30
            let json_value = json_mod::from_str(content)?;
40,492✔
31
            Ok(SectionContent::Json(json_value))
40,492✔
32
        }
33
        SectionType::Response => {
34
            // Primary mode: a single JSON/JSON5 value
35
            if let Ok(json_value) = json_mod::from_str(content) {
40,473✔
36
                return Ok(SectionContent::Json(json_value));
40,460✔
37
            }
13✔
38

39
            // Streaming mode: multiple JSON payloads within one RESPONSE block
40
            if let Some(values) = json_stream_parser::parse_response_json_values(content) {
13✔
41
                Ok(SectionContent::JsonLines(values))
13✔
42
            } else {
43
                // Preserve original parse error behavior for malformed single-content responses
NEW
44
                let json_value = json_mod::from_str(content)?;
×
NEW
45
                Ok(SectionContent::Json(json_value))
×
46
            }
47
        }
48

49
        // Key-value sections
50
        SectionType::RequestHeaders
51
        | SectionType::Tls
52
        | SectionType::Proto
53
        | SectionType::Options => {
54
            let key_values = parse_key_value_section(content)?;
34✔
55
            Ok(SectionContent::KeyValues(key_values))
34✔
56
        }
57

58
        // Extract section - support ternary expressions via AST
59
        SectionType::Extract => {
60
            let mut key_values = std::collections::HashMap::new();
10,871✔
61
            for line in content.lines() {
10,962✔
62
                if let Some((name, value)) = tokenize_extract_line(line)
10,962✔
63
                    && let Some(extract_var) =
10,935✔
64
                        crate::parser::ternary_ast::ExtractVar::parse_raw(&name, &value)
10,935✔
65
                {
10,935✔
66
                    key_values.insert(extract_var.name, extract_var.value.to_jq());
10,935✔
67
                }
10,935✔
68
            }
69
            Ok(SectionContent::Extract(key_values))
10,871✔
70
        }
71

72
        // Assertion sections
73
        SectionType::Asserts => {
74
            let assertions = parse_assertions(content)?;
134✔
75
            Ok(SectionContent::Assertions(assertions))
134✔
76
        }
77

78
        // META section - parse as YAML (comments allowed)
79
        SectionType::Meta => {
80
            let meta = serde_yaml_ng::from_str::<FileMeta>(content)
2✔
81
                .unwrap_or_else(|_| FileMeta::default());
2✔
82
            Ok(SectionContent::Meta(meta))
2✔
83
        }
84
    }
85
}
132,542✔
86

87
/// Build a section from parsed content.
88
pub fn build_section(
132,526✔
89
    section_type: SectionType,
132,526✔
90
    start_line: usize,
132,526✔
91
    end_line: usize,
132,526✔
92
    content: &[String],
132,526✔
93
    inline_options: InlineOptions,
132,526✔
94
) -> Result<Section> {
132,526✔
95
    let raw_content = content.join("\n");
132,526✔
96
    let section_content = parse_section_content(section_type, &raw_content)?;
132,526✔
97

98
    Ok(Section {
132,526✔
99
        section_type,
132,526✔
100
        content: section_content,
132,526✔
101
        inline_options,
132,526✔
102
        raw_content,
132,526✔
103
        start_line,
132,526✔
104
        end_line,
132,526✔
105
    })
132,526✔
106
}
132,526✔
107

108
/// Parse key=value options from section header inline options string.
109
pub fn parse_inline_options(s: &str) -> Result<InlineOptions> {
69✔
110
    let mut inline_options = InlineOptions::default();
69✔
111

112
    for (key, value) in tokenize_inline_options(s) {
88✔
113
        match key.as_str() {
88✔
114
            "with_asserts" => {
88✔
115
                inline_options.with_asserts = matches!(value.as_str(), "true" | "1");
31✔
116
            }
117
            "partial" => {
57✔
118
                inline_options.partial = matches!(value.as_str(), "true" | "1");
26✔
119
            }
120
            "tolerance" => {
31✔
121
                if let Ok(t) = value.parse::<f64>() {
13✔
122
                    inline_options.tolerance = Some(t);
12✔
123
                }
12✔
124
            }
125
            "redact" => {
18✔
126
                let redact_str = value.trim().trim_matches('[').trim_matches(']');
6✔
127
                let strings: Vec<String> = redact_str
6✔
128
                    .split(',')
6✔
129
                    .map(|s| s.trim().trim_matches('"').to_string())
10✔
130
                    .filter(|s| !s.is_empty())
10✔
131
                    .collect();
6✔
132
                inline_options.redact = strings;
6✔
133
            }
134
            "unordered_arrays" => {
12✔
135
                inline_options.unordered_arrays = matches!(value.as_str(), "true" | "1");
11✔
136
            }
137
            _ => {}
1✔
138
        }
139
    }
140

141
    Ok(inline_options)
69✔
142
}
69✔
143

144
/// Parse key-value section (one per line: key: value).
145
fn parse_key_value_section(content: &str) -> Result<std::collections::HashMap<String, String>> {
34✔
146
    let mut key_values = std::collections::HashMap::new();
34✔
147

148
    for line in content.lines() {
61✔
149
        if let Some((key, value)) = tokenize_kv_line(line) {
61✔
150
            key_values.insert(key, value);
59✔
151
        }
59✔
152
    }
153

154
    Ok(key_values)
34✔
155
}
34✔
156

157
/// Parse assertions section (one assertion per line).
158
fn parse_assertions(content: &str) -> Result<Vec<String>> {
134✔
159
    use crate::parser::assertions::strip_assertion_comments;
160

161
    // No normalization needed — regex literals /pattern/ are now handled
162
    // by the assertion AST parser as Expr::RegExp nodes.
163
    let assertions: Vec<String> = content
134✔
164
        .lines()
134✔
165
        .filter_map(strip_assertion_comments)
134✔
166
        .map(|line| line.to_string())
181✔
167
        .collect();
134✔
168

169
    Ok(assertions)
134✔
170
}
134✔
171

172
#[cfg(test)]
173
mod tests {
174
    use super::*;
175

176
    #[test]
177
    fn test_tokenize_options() {
1✔
178
        let result = tokenize_inline_options("key1=value1 key2=value2");
1✔
179
        assert_eq!(result.len(), 2);
1✔
180
        assert_eq!(result[0], ("key1".into(), "value1".into()));
1✔
181
        assert_eq!(result[1], ("key2".into(), "value2".into()));
1✔
182
    }
1✔
183

184
    #[test]
185
    fn test_parse_inline_options() {
1✔
186
        let result = parse_inline_options("with_asserts=true partial=false tolerance=0.1").unwrap();
1✔
187
        assert!(result.with_asserts);
1✔
188
        assert!(!result.partial);
1✔
189
        assert_eq!(result.tolerance, Some(0.1));
1✔
190
    }
1✔
191

192
    #[test]
193
    fn test_parse_section_content_single_value() {
1✔
194
        let result = parse_section_content(SectionType::Address, "localhost:50051").unwrap();
1✔
195
        assert_eq!(
1✔
196
            result,
197
            SectionContent::Single("localhost:50051".to_string())
1✔
198
        );
199
    }
1✔
200

201
    #[test]
202
    fn test_parse_section_content_empty() {
1✔
203
        let result = parse_section_content(SectionType::Address, "").unwrap();
1✔
204
        assert_eq!(result, SectionContent::Empty);
1✔
205
    }
1✔
206

207
    #[test]
208
    fn test_parse_section_content_whitespace() {
1✔
209
        let result = parse_section_content(SectionType::Address, "   ").unwrap();
1✔
210
        assert_eq!(result, SectionContent::Empty);
1✔
211
    }
1✔
212

213
    #[test]
214
    fn test_parse_section_content_endpoint() {
1✔
215
        let result = parse_section_content(SectionType::Endpoint, "pkg.Service/Method").unwrap();
1✔
216
        assert_eq!(
1✔
217
            result,
218
            SectionContent::Single("pkg.Service/Method".to_string())
1✔
219
        );
220
    }
1✔
221

222
    #[test]
223
    fn test_parse_section_content_request_json() {
1✔
224
        let result = parse_section_content(SectionType::Request, r#"{"key": "value"}"#).unwrap();
1✔
225
        assert!(matches!(result, SectionContent::Json(_)));
1✔
226
    }
1✔
227

228
    #[test]
229
    fn test_parse_section_content_error_json() {
1✔
230
        let result = parse_section_content(SectionType::Error, r#"{"code": 5}"#).unwrap();
1✔
231
        assert!(matches!(result, SectionContent::Json(_)));
1✔
232
    }
1✔
233

234
    #[test]
235
    fn test_parse_section_content_response_json() {
1✔
236
        let result = parse_section_content(SectionType::Response, r#"{"status": "ok"}"#).unwrap();
1✔
237
        assert!(matches!(result, SectionContent::Json(_)));
1✔
238
    }
1✔
239

240
    #[test]
241
    fn test_parse_section_content_response_jsonlines() {
1✔
242
        let input = "{\"a\":1}\n{\"b\":2}";
1✔
243
        let result = parse_section_content(SectionType::Response, input).unwrap();
1✔
244
        assert!(matches!(result, SectionContent::JsonLines(v) if v.len() == 2));
1✔
245
    }
1✔
246

247
    #[test]
248
    fn test_parse_section_content_key_values() {
1✔
249
        let input = "ca_cert: /path/to/ca.pem\nserver_name: example.com";
1✔
250
        let result = parse_section_content(SectionType::Tls, input).unwrap();
1✔
251
        if let SectionContent::KeyValues(kv) = result {
1✔
252
            assert_eq!(kv.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1✔
253
            assert_eq!(kv.get("server_name"), Some(&"example.com".to_string()));
1✔
254
        } else {
NEW
255
            panic!("expected KeyValues");
×
256
        }
257
    }
1✔
258

259
    #[test]
260
    fn test_parse_section_content_key_values_with_comments() {
1✔
261
        let input = "# comment\nca_cert: /path/ca.pem\n\nkey: value";
1✔
262
        let result = parse_section_content(SectionType::Options, input).unwrap();
1✔
263
        if let SectionContent::KeyValues(kv) = result {
1✔
264
            assert_eq!(kv.len(), 2);
1✔
265
        } else {
NEW
266
            panic!("expected KeyValues");
×
267
        }
268
    }
1✔
269

270
    #[test]
271
    fn test_parse_section_content_extract() {
1✔
272
        let input = "total = .response.total\ncount = .items | length";
1✔
273
        let result = parse_section_content(SectionType::Extract, input).unwrap();
1✔
274
        if let SectionContent::Extract(kv) = result {
1✔
275
            assert_eq!(kv.get("total"), Some(&".response.total".to_string()));
1✔
276
            assert!(kv.contains_key("count"));
1✔
277
        } else {
NEW
278
            panic!("expected Extract");
×
279
        }
280
    }
1✔
281

282
    #[test]
283
    fn test_parse_section_content_extract_with_comments() {
1✔
284
        let input = "# ignore\n// ignore\ntotal = .response.total";
1✔
285
        let result = parse_section_content(SectionType::Extract, input).unwrap();
1✔
286
        if let SectionContent::Extract(kv) = result {
1✔
287
            assert_eq!(kv.len(), 1);
1✔
288
        } else {
NEW
289
            panic!("expected Extract");
×
290
        }
291
    }
1✔
292

293
    #[test]
294
    fn test_parse_section_content_asserts() {
1✔
295
        let input = ".x == 1\n.y != \"hello\"";
1✔
296
        let result = parse_section_content(SectionType::Asserts, input).unwrap();
1✔
297
        if let SectionContent::Assertions(asserts) = result {
1✔
298
            assert_eq!(asserts.len(), 2);
1✔
299
            assert_eq!(asserts[0], ".x == 1");
1✔
300
        } else {
NEW
301
            panic!("expected Assertions");
×
302
        }
303
    }
1✔
304

305
    #[test]
306
    fn test_parse_section_content_asserts_with_comments() {
1✔
307
        let input = ".x == 1 # inline\n# full line\n.y == 2 // comment";
1✔
308
        let result = parse_section_content(SectionType::Asserts, input).unwrap();
1✔
309
        if let SectionContent::Assertions(asserts) = result {
1✔
310
            assert_eq!(asserts.len(), 2);
1✔
311
        } else {
NEW
312
            panic!("expected Assertions");
×
313
        }
314
    }
1✔
315

316
    #[test]
317
    fn test_build_section() {
1✔
318
        let content = vec!["localhost:50051".to_string()];
1✔
319
        let section = build_section(
1✔
320
            SectionType::Address,
1✔
321
            5,
322
            6,
323
            &content,
1✔
324
            InlineOptions::default(),
1✔
325
        )
326
        .unwrap();
1✔
327
        assert_eq!(section.section_type, SectionType::Address);
1✔
328
        assert_eq!(section.start_line, 5);
1✔
329
        assert_eq!(section.end_line, 6);
1✔
330
    }
1✔
331

332
    #[test]
333
    fn test_parse_inline_options_all_fields() {
1✔
334
        let result = parse_inline_options(
1✔
335
            "with_asserts=true partial=true tolerance=0.5 unordered_arrays=true",
1✔
336
        )
337
        .unwrap();
1✔
338
        assert!(result.with_asserts);
1✔
339
        assert!(result.partial);
1✔
340
        assert_eq!(result.tolerance, Some(0.5));
1✔
341
        assert!(result.unordered_arrays);
1✔
342
    }
1✔
343

344
    #[test]
345
    fn test_parse_inline_options_redact() {
1✔
346
        let result = parse_inline_options(r#"redact=["token","password"]"#).unwrap();
1✔
347
        assert_eq!(result.redact, vec!["token", "password"]);
1✔
348
    }
1✔
349

350
    #[test]
351
    fn test_parse_inline_options_empty() {
1✔
352
        let result = parse_inline_options("").unwrap();
1✔
353
        assert_eq!(result, InlineOptions::default());
1✔
354
    }
1✔
355

356
    #[test]
357
    fn test_parse_inline_options_unknown_key_ignored() {
1✔
358
        let result = parse_inline_options("unknown_key=value").unwrap();
1✔
359
        assert_eq!(result, InlineOptions::default());
1✔
360
    }
1✔
361

362
    #[test]
363
    fn test_parse_inline_options_tolerance_negative() {
1✔
364
        let result = parse_inline_options("tolerance=-0.5").unwrap();
1✔
365
        assert_eq!(result.tolerance, Some(-0.5));
1✔
366
    }
1✔
367

368
    #[test]
369
    fn test_parse_inline_options_tolerance_invalid() {
1✔
370
        let result = parse_inline_options("tolerance=not_a_number").unwrap();
1✔
371
        assert_eq!(result.tolerance, None);
1✔
372
    }
1✔
373

374
    #[test]
375
    fn test_parse_inline_options_redact_empty_array() {
1✔
376
        let result = parse_inline_options("redact=[]").unwrap();
1✔
377
        assert!(result.redact.is_empty());
1✔
378
    }
1✔
379

380
    #[test]
381
    fn test_parse_inline_options_redact_malformed() {
1✔
382
        let result = parse_inline_options("redact=not_an_array").unwrap();
1✔
383
        // Current tokenizer splits by spaces, so this becomes tokens
384
        // This is a known limitation - redact with spaces in value
385
        assert!(!result.redact.is_empty()); // tokenizer splits "not_an_array" into parts
1✔
386
    }
1✔
387

388
    #[test]
389
    fn test_parse_section_content_meta_full() {
1✔
390
        let result = parse_section_content(
1✔
391
            SectionType::Meta,
1✔
392
            r#"name: Test
1✔
393
summary: Summary
1✔
394
tags: [a, b]
1✔
395
owner: backend
1✔
396
links:
1✔
397
  - https://example.com
1✔
398
"#,
1✔
399
        )
400
        .unwrap();
1✔
401
        let SectionContent::Meta(m) = result else {
1✔
NEW
402
            panic!()
×
403
        };
404
        assert_eq!(m.name.as_deref(), Some("Test"));
1✔
405
        assert_eq!(m.summary.as_deref(), Some("Summary"));
1✔
406
        assert_eq!(m.tags, ["a", "b"]);
1✔
407
        assert_eq!(m.owner.as_deref(), Some("backend"));
1✔
408
        assert_eq!(m.links, ["https://example.com"]);
1✔
409
    }
1✔
410

411
    #[test]
412
    fn test_parse_section_content_meta_comments() {
1✔
413
        let result = parse_section_content(
1✔
414
            SectionType::Meta,
1✔
415
            r#"# comment
1✔
416
name: Test
1✔
417
tags: [a]
1✔
418
"#,
1✔
419
        )
420
        .unwrap();
1✔
421
        let SectionContent::Meta(m) = result else {
1✔
NEW
422
            panic!()
×
423
        };
424
        assert_eq!(m.name.as_deref(), Some("Test"));
1✔
425
        assert_eq!(m.tags, ["a"]);
1✔
426
    }
1✔
427
}
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