• 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

97.36
/src/parser/gctf_tokenizer.rs
1
//! Universal GCTF file tokenizer.
2
//!
3
//! This is the **only** module that reads raw `.gctf` text.
4
//! Pipeline: `text → tokenize_gctf() → Vec<GctfToken> → parser → AST`
5
//!
6
//! IMPLEMENTATION NOTES:
7
//! - No regex, no starts_with, no ends_with, no contains, no find()
8
//! - Pure byte-level scanning with exact span tracking
9
//! - Only uses .as_bytes() once per line to get byte slice for scanning
10
//! - Only uses .to_string() to create owned output (necessary for API)
11
//! - All "parsing" done via byte comparisons (bytes[pos] == b'-')
12
//!
13
//! String operations count: 0 (only .to_string() for output ownership)
14

15
use crate::parser::tokenizer::Span;
16

17
#[derive(Debug, Clone, PartialEq)]
18
pub struct GctfToken {
19
    pub kind: GctfTokenKind,
20
    pub line: usize,
21
    pub span: Span,
22
}
23

24
#[derive(Debug, Clone, PartialEq)]
25
pub enum GctfTokenKind {
26
    SectionHeader { name: String, raw_options: String },
27
    Comment(String),
28
    Blank,
29
    Content(String),
30
}
31

32
pub fn tokenize_gctf(source: &str) -> Vec<GctfToken> {
11,454✔
33
    let mut tokens = Vec::new();
11,454✔
34
    for (line_idx, line) in source.lines().enumerate() {
367,115✔
35
        tokens.push(classify_line(line_idx, line));
367,115✔
36
    }
367,115✔
37
    tokens
11,454✔
38
}
11,454✔
39

40
fn classify_line(line_idx: usize, line: &str) -> GctfToken {
367,115✔
41
    let bytes = line.as_bytes();
367,115✔
42
    let len = bytes.len();
367,115✔
43
    let mut pos = 0;
367,115✔
44

45
    while pos < len && is_ws(bytes[pos]) {
368,023✔
46
        pos += 1;
908✔
47
    }
908✔
48

49
    if pos == len {
367,115✔
50
        return GctfToken {
101,141✔
51
            kind: GctfTokenKind::Blank,
101,141✔
52
            line: line_idx,
101,141✔
53
            span: Span { start: 0, end: len },
101,141✔
54
        };
101,141✔
55
    }
265,974✔
56

57
    if bytes[pos] == b'#' {
265,974✔
58
        return GctfToken {
36✔
59
            kind: GctfTokenKind::Comment(line.to_string()),
36✔
60
            line: line_idx,
36✔
61
            span: Span { start: 0, end: len },
36✔
62
        };
36✔
63
    }
265,938✔
64

65
    if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
265,938✔
66
        return GctfToken {
29✔
67
            kind: GctfTokenKind::Comment(line.to_string()),
29✔
68
            line: line_idx,
29✔
69
            span: Span { start: 0, end: len },
29✔
70
        };
29✔
71
    }
265,909✔
72

73
    if let Some(header) = scan_section_header(line_idx, line, bytes, len) {
265,909✔
74
        return header;
132,546✔
75
    }
133,363✔
76

77
    GctfToken {
133,363✔
78
        kind: GctfTokenKind::Content(line.to_string()),
133,363✔
79
        line: line_idx,
133,363✔
80
        span: Span { start: 0, end: len },
133,363✔
81
    }
133,363✔
82
}
367,115✔
83

84
fn scan_section_header(line_idx: usize, line: &str, bytes: &[u8], len: usize) -> Option<GctfToken> {
265,909✔
85
    let mut pos = 0;
265,909✔
86

87
    while pos < len && is_ws(bytes[pos]) {
266,799✔
88
        pos += 1;
890✔
89
    }
890✔
90

91
    if pos + 2 >= len || bytes[pos] != b'-' || bytes[pos + 1] != b'-' || bytes[pos + 2] != b'-' {
265,909✔
92
        return None;
133,356✔
93
    }
132,553✔
94
    pos += 3;
132,553✔
95

96
    while pos < len && is_ws(bytes[pos]) {
265,105✔
97
        pos += 1;
132,552✔
98
    }
132,552✔
99

100
    let name_start = pos;
132,553✔
101
    while pos < len && is_section_name_char(bytes[pos]) {
1,141,446✔
102
        pos += 1;
1,008,893✔
103
    }
1,008,893✔
104
    if pos == name_start {
132,553✔
105
        return None;
4✔
106
    }
132,549✔
107
    let name = slice_str(line, name_start, pos);
132,549✔
108

109
    let mut trailing = len;
132,549✔
110
    while trailing > pos && is_ws(bytes[trailing - 1]) {
132,551✔
111
        trailing -= 1;
2✔
112
    }
2✔
113
    if trailing < pos + 3
132,549✔
114
        || bytes[trailing - 3] != b'-'
132,546✔
115
        || bytes[trailing - 2] != b'-'
132,546✔
116
        || bytes[trailing - 1] != b'-'
132,546✔
117
    {
118
        return None;
3✔
119
    }
132,546✔
120
    let options_end = trailing - 3;
132,546✔
121

122
    let mut opts_start = pos;
132,546✔
123
    while opts_start < options_end && is_ws(bytes[opts_start]) {
265,093✔
124
        opts_start += 1;
132,547✔
125
    }
132,547✔
126
    let mut opts_end = options_end;
132,546✔
127
    while opts_end > opts_start && is_ws(bytes[opts_end - 1]) {
132,612✔
128
        opts_end -= 1;
66✔
129
    }
66✔
130

131
    let raw_options = if opts_start < opts_end {
132,546✔
132
        slice_str(line, opts_start, opts_end)
64✔
133
    } else {
134
        String::new()
132,482✔
135
    };
136

137
    Some(GctfToken {
132,546✔
138
        kind: GctfTokenKind::SectionHeader { name, raw_options },
132,546✔
139
        line: line_idx,
132,546✔
140
        span: Span { start: 0, end: len },
132,546✔
141
    })
132,546✔
142
}
265,909✔
143

144
pub fn tokenize_kv_line(line: &str) -> Option<(String, String)> {
73✔
145
    let bytes = line.as_bytes();
73✔
146
    let len = bytes.len();
73✔
147
    let mut pos = 0;
73✔
148

149
    while pos < len && is_ws(bytes[pos]) {
83✔
150
        pos += 1;
10✔
151
    }
10✔
152

153
    if pos == len {
73✔
154
        return None;
3✔
155
    }
70✔
156
    if bytes[pos] == b'#' {
70✔
157
        return None;
2✔
158
    }
68✔
159
    if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
68✔
160
        return None;
1✔
161
    }
67✔
162

163
    let key_start = pos;
67✔
164
    while pos < len && bytes[pos] != b':' {
689✔
165
        pos += 1;
622✔
166
    }
622✔
167
    if pos == len {
67✔
168
        return None;
1✔
169
    }
66✔
170

171
    let mut key_end = pos;
66✔
172
    while key_end > key_start && is_ws(bytes[key_end - 1]) {
71✔
173
        key_end -= 1;
5✔
174
    }
5✔
175
    pos += 1;
66✔
176

177
    while pos < len && is_ws(bytes[pos]) {
133✔
178
        pos += 1;
67✔
179
    }
67✔
180

181
    let val_start = pos;
66✔
182
    let mut val_end = len;
66✔
183
    while val_end > val_start && is_ws(bytes[val_end - 1]) {
70✔
184
        val_end -= 1;
4✔
185
    }
4✔
186

187
    Some((
66✔
188
        slice_str(line, key_start, key_end),
66✔
189
        slice_str(line, val_start, val_end),
66✔
190
    ))
66✔
191
}
73✔
192

193
pub fn tokenize_extract_line(line: &str) -> Option<(String, String)> {
10,971✔
194
    let bytes = line.as_bytes();
10,971✔
195
    let len = bytes.len();
10,971✔
196
    let mut pos = 0;
10,971✔
197

198
    while pos < len && is_ws(bytes[pos]) {
10,976✔
199
        pos += 1;
5✔
200
    }
5✔
201

202
    if pos == len {
10,971✔
203
        return None;
10✔
204
    }
10,961✔
205
    if bytes[pos] == b'#' {
10,961✔
206
        return None;
13✔
207
    }
10,948✔
208
    if pos + 1 < len && bytes[pos] == b'/' && bytes[pos + 1] == b'/' {
10,948✔
209
        return None;
2✔
210
    }
10,946✔
211

212
    let name_start = pos;
10,946✔
213
    while pos < len && bytes[pos] != b'=' {
86,453✔
214
        pos += 1;
75,507✔
215
    }
75,507✔
216
    if pos == len {
10,946✔
217
        return None;
7✔
218
    }
10,939✔
219

220
    let mut name_end = pos;
10,939✔
221
    while name_end > name_start && is_ws(bytes[name_end - 1]) {
21,877✔
222
        name_end -= 1;
10,938✔
223
    }
10,938✔
224
    pos += 1;
10,939✔
225

226
    while pos < len && is_ws(bytes[pos]) {
21,880✔
227
        pos += 1;
10,941✔
228
    }
10,941✔
229

230
    let val_start = pos;
10,939✔
231
    let mut val_end = len;
10,939✔
232
    while val_end > val_start && is_ws(bytes[val_end - 1]) {
10,941✔
233
        val_end -= 1;
2✔
234
    }
2✔
235

236
    Some((
10,939✔
237
        slice_str(line, name_start, name_end),
10,939✔
238
        slice_str(line, val_start, val_end),
10,939✔
239
    ))
10,939✔
240
}
10,971✔
241

242
pub fn tokenize_inline_options(raw: &str) -> Vec<(String, String)> {
83✔
243
    let bytes = raw.as_bytes();
83✔
244
    let len = bytes.len();
83✔
245
    let mut pos = 0;
83✔
246
    let mut result = Vec::new();
83✔
247

248
    while pos < len {
190✔
249
        while pos < len && is_ws(bytes[pos]) {
145✔
250
            pos += 1;
36✔
251
        }
36✔
252
        if pos >= len {
109✔
253
            break;
2✔
254
        }
107✔
255

256
        let tok_start = pos;
107✔
257
        let mut in_quotes = false;
107✔
258
        let mut escaped = false;
107✔
259

260
        while pos < len {
1,682✔
261
            if escaped {
1,603✔
262
                escaped = false;
1✔
263
                pos += 1;
1✔
264
                continue;
1✔
265
            }
1,602✔
266
            match bytes[pos] {
1,602✔
267
                b'\\' => {
1✔
268
                    escaped = true;
1✔
269
                    pos += 1;
1✔
270
                }
1✔
271
                b'"' => {
20✔
272
                    in_quotes = !in_quotes;
20✔
273
                    pos += 1;
20✔
274
                }
20✔
275
                b' ' | b'\t' if !in_quotes => break,
28✔
276
                _ => pos += 1,
1,553✔
277
            }
278
        }
279
        let tok_end = pos;
107✔
280

281
        let token = slice_str(raw, tok_start, tok_end);
107✔
282

283
        let mut eq_pos = None;
107✔
284
        let tb = token.as_bytes();
107✔
285
        for (i, &b) in tb.iter().enumerate() {
1,080✔
286
            if b == b'=' {
1,080✔
287
                eq_pos = Some(i);
102✔
288
                break;
102✔
289
            }
978✔
290
        }
291

292
        if let Some(eq) = eq_pos {
107✔
293
            let mut key_end = eq;
102✔
294
            while key_end > 0 && is_ws(tb[key_end - 1]) {
102✔
NEW
295
                key_end -= 1;
×
NEW
296
            }
×
297
            let mut val_start = eq + 1;
102✔
298
            while val_start < tb.len() && is_ws(tb[val_start]) {
102✔
NEW
299
                val_start += 1;
×
NEW
300
            }
×
301
            let mut val_end = tb.len();
102✔
302
            while val_end > val_start && is_ws(tb[val_end - 1]) {
102✔
NEW
303
                val_end -= 1;
×
NEW
304
            }
×
305

306
            let mut key = slice_str(&token, 0, key_end);
102✔
307
            let mut value = slice_str(&token, val_start, val_end);
102✔
308

309
            strip_outer_quotes(&mut key);
102✔
310
            strip_outer_quotes(&mut value);
102✔
311

312
            result.push((key, value));
102✔
313
        } else {
5✔
314
            let mut key = token;
5✔
315
            strip_outer_quotes(&mut key);
5✔
316
            result.push((key, "true".to_string()));
5✔
317
        }
5✔
318
    }
319

320
    result
83✔
321
}
83✔
322

323
fn strip_outer_quotes(s: &mut String) {
209✔
324
    if s.len() >= 2 {
209✔
325
        let b = s.as_bytes();
200✔
326
        if (b[0] == b'"' && b[s.len() - 1] == b'"') || (b[0] == b'\'' && b[s.len() - 1] == b'\'') {
200✔
327
            *s = s[1..s.len() - 1].to_string();
2✔
328
        }
198✔
329
    }
9✔
330
}
209✔
331

332
#[inline]
333
fn is_ws(b: u8) -> bool {
1,130,533✔
334
    matches!(b, b' ' | b'\t')
1,130,533✔
335
}
1,130,533✔
336

337
#[inline]
338
fn is_section_name_char(b: u8) -> bool {
1,141,443✔
339
    b.is_ascii_uppercase() || b == b'_'
1,141,443✔
340
}
1,141,443✔
341

342
#[inline]
343
fn slice_str(s: &str, start: usize, end: usize) -> String {
154,934✔
344
    if start >= end {
154,934✔
345
        return String::new();
5✔
346
    }
154,929✔
347
    s[start..end].to_string()
154,929✔
348
}
154,934✔
349

350
#[cfg(test)]
351
mod tests {
352
    use super::*;
353

354
    #[test]
355
    fn test_tokenize_empty() {
1✔
356
        let tokens = tokenize_gctf("");
1✔
357
        assert!(tokens.is_empty());
1✔
358
    }
1✔
359

360
    #[test]
361
    fn test_tokenize_blank_lines() {
1✔
362
        let tokens = tokenize_gctf("\n\n  \n");
1✔
363
        assert_eq!(tokens.len(), 3);
1✔
364
        assert!(matches!(tokens[0].kind, GctfTokenKind::Blank));
1✔
365
        assert!(matches!(tokens[1].kind, GctfTokenKind::Blank));
1✔
366
        assert!(matches!(tokens[2].kind, GctfTokenKind::Blank));
1✔
367
    }
1✔
368

369
    #[test]
370
    fn test_tokenize_comments() {
1✔
371
        let tokens = tokenize_gctf("# hello\n// world");
1✔
372
        assert_eq!(tokens.len(), 2);
1✔
373
        assert!(matches!(&tokens[0].kind, GctfTokenKind::Comment(t) if t == "# hello"));
1✔
374
        assert!(matches!(&tokens[1].kind, GctfTokenKind::Comment(t) if t == "// world"));
1✔
375
    }
1✔
376

377
    #[test]
378
    fn test_tokenize_section_headers() {
1✔
379
        let tokens = tokenize_gctf("--- ENDPOINT ---\n--- RESPONSE partial=true ---");
1✔
380
        assert_eq!(tokens.len(), 2);
1✔
381

382
        match &tokens[0].kind {
1✔
383
            GctfTokenKind::SectionHeader { name, raw_options } => {
1✔
384
                assert_eq!(name, "ENDPOINT");
1✔
385
                assert_eq!(raw_options, "");
1✔
386
            }
NEW
387
            _ => panic!("expected SectionHeader"),
×
388
        }
389

390
        match &tokens[1].kind {
1✔
391
            GctfTokenKind::SectionHeader { name, raw_options } => {
1✔
392
                assert_eq!(name, "RESPONSE");
1✔
393
                assert_eq!(raw_options, "partial=true");
1✔
394
            }
NEW
395
            _ => panic!("expected SectionHeader"),
×
396
        }
397
    }
1✔
398

399
    #[test]
400
    fn test_tokenize_content() {
1✔
401
        let tokens = tokenize_gctf("hello world\n{\"key\": \"value\"}");
1✔
402
        assert_eq!(tokens.len(), 2);
1✔
403
        assert!(matches!(&tokens[0].kind, GctfTokenKind::Content(t) if t == "hello world"));
1✔
404
        assert!(
1✔
405
            matches!(&tokens[1].kind, GctfTokenKind::Content(t) if t == "{\"key\": \"value\"}")
1✔
406
        );
407
    }
1✔
408

409
    #[test]
410
    fn test_tokenize_not_section_header() {
1✔
411
        let tokens = tokenize_gctf("--- not uppercase ---\n---ABC");
1✔
412
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
413
        assert!(matches!(tokens[1].kind, GctfTokenKind::Content(_)));
1✔
414
    }
1✔
415

416
    #[test]
417
    fn test_tokenize_full_document() {
1✔
418
        let input = r#"--- ENDPOINT ---
1✔
419
test.Service/Method
1✔
420

1✔
421
--- REQUEST ---
1✔
422
{}
1✔
423

1✔
424
--- ASSERTS ---
1✔
425
.x == 1
1✔
426
"#;
1✔
427
        let tokens = tokenize_gctf(input);
1✔
428
        let mut kinds: Vec<&str> = Vec::new();
1✔
429
        for t in &tokens {
8✔
430
            match &t.kind {
8✔
431
                GctfTokenKind::SectionHeader { .. } => kinds.push("H"),
3✔
NEW
432
                GctfTokenKind::Comment(_) => kinds.push("C"),
×
433
                GctfTokenKind::Blank => kinds.push("_"),
2✔
434
                GctfTokenKind::Content(_) => kinds.push("T"),
3✔
435
            }
436
        }
437
        assert_eq!(kinds, vec!["H", "T", "_", "H", "T", "_", "H", "T"]);
1✔
438
    }
1✔
439

440
    #[test]
441
    fn test_tokenize_section_header_with_multiple_options() {
1✔
442
        let tokens = tokenize_gctf("--- RESPONSE partial=true tolerance=0.1 ---");
1✔
443
        match &tokens[0].kind {
1✔
444
            GctfTokenKind::SectionHeader { name, raw_options } => {
1✔
445
                assert_eq!(name, "RESPONSE");
1✔
446
                assert_eq!(raw_options, "partial=true tolerance=0.1");
1✔
447
            }
NEW
448
            _ => panic!("expected SectionHeader"),
×
449
        }
450
    }
1✔
451

452
    #[test]
453
    fn test_kv_line_basic() {
1✔
454
        let (key, value) = tokenize_kv_line("Authorization: Bearer token").unwrap();
1✔
455
        assert_eq!(key, "Authorization");
1✔
456
        assert_eq!(value, "Bearer token");
1✔
457
    }
1✔
458

459
    #[test]
460
    fn test_kv_line_with_whitespace() {
1✔
461
        let (key, value) = tokenize_kv_line("  key  :  value  ").unwrap();
1✔
462
        assert_eq!(key, "key");
1✔
463
        assert_eq!(value, "value");
1✔
464
    }
1✔
465

466
    #[test]
467
    fn test_kv_line_comment() {
1✔
468
        assert_eq!(tokenize_kv_line("# comment"), None);
1✔
469
        assert_eq!(tokenize_kv_line("// comment"), None);
1✔
470
    }
1✔
471

472
    #[test]
473
    fn test_kv_line_empty() {
1✔
474
        assert_eq!(tokenize_kv_line(""), None);
1✔
475
        assert_eq!(tokenize_kv_line("   "), None);
1✔
476
    }
1✔
477

478
    #[test]
479
    fn test_kv_line_no_colon() {
1✔
480
        assert_eq!(tokenize_kv_line("no colon here"), None);
1✔
481
    }
1✔
482

483
    #[test]
484
    fn test_extract_line_basic() {
1✔
485
        let (name, value) = tokenize_extract_line("total = .response.total").unwrap();
1✔
486
        assert_eq!(name, "total");
1✔
487
        assert_eq!(value, ".response.total");
1✔
488
    }
1✔
489

490
    #[test]
491
    fn test_extract_line_comment() {
1✔
492
        assert_eq!(tokenize_extract_line("# comment"), None);
1✔
493
        assert_eq!(tokenize_extract_line("// comment"), None);
1✔
494
    }
1✔
495

496
    #[test]
497
    fn test_extract_line_empty() {
1✔
498
        assert_eq!(tokenize_extract_line(""), None);
1✔
499
    }
1✔
500

501
    #[test]
502
    fn test_tokenize_inline_options_basic() {
1✔
503
        let opts = tokenize_inline_options("key1=value1 key2=value2");
1✔
504
        assert_eq!(opts.len(), 2);
1✔
505
        assert_eq!(opts[0], ("key1".into(), "value1".into()));
1✔
506
        assert_eq!(opts[1], ("key2".into(), "value2".into()));
1✔
507
    }
1✔
508

509
    #[test]
510
    fn test_tokenize_inline_options_quoted() {
1✔
511
        let opts = tokenize_inline_options(r#"key="hello world""#);
1✔
512
        assert_eq!(opts.len(), 1);
1✔
513
        assert_eq!(opts[0], ("key".into(), "hello world".into()));
1✔
514
    }
1✔
515

516
    #[test]
517
    fn test_tokenize_inline_options_boolean_short_form() {
1✔
518
        let opts = tokenize_inline_options("partial");
1✔
519
        assert_eq!(opts.len(), 1);
1✔
520
        assert_eq!(opts[0], ("partial".into(), "true".into()));
1✔
521
    }
1✔
522

523
    #[test]
524
    fn test_tokenize_inline_options_complex() {
1✔
525
        let opts = tokenize_inline_options("with_asserts=true partial=false tolerance=0.1");
1✔
526
        assert_eq!(opts.len(), 3);
1✔
527
        assert_eq!(opts[0], ("with_asserts".into(), "true".into()));
1✔
528
        assert_eq!(opts[1], ("partial".into(), "false".into()));
1✔
529
        assert_eq!(opts[2], ("tolerance".into(), "0.1".into()));
1✔
530
    }
1✔
531

532
    #[test]
533
    fn test_line_numbers() {
1✔
534
        let tokens = tokenize_gctf("line0\nline1\nline2");
1✔
535
        assert_eq!(tokens[0].line, 0);
1✔
536
        assert_eq!(tokens[1].line, 1);
1✔
537
        assert_eq!(tokens[2].line, 2);
1✔
538
    }
1✔
539

540
    // === scan_section_header edge cases ===
541

542
    #[test]
543
    fn test_section_header_meta() {
1✔
544
        let tokens = tokenize_gctf("--- META ---\nname: Test");
1✔
545
        let GctfTokenKind::SectionHeader { name, .. } = &tokens[0].kind else {
1✔
NEW
546
            panic!()
×
547
        };
548
        assert_eq!(name, "META");
1✔
549
    }
1✔
550

551
    #[test]
552
    fn test_section_header_no_closing_dashes() {
1✔
553
        let tokens = tokenize_gctf("--- ENDPOINT");
1✔
554
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
555
    }
1✔
556

557
    #[test]
558
    fn test_section_header_only_dashes() {
1✔
559
        let tokens = tokenize_gctf("------");
1✔
560
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
561
    }
1✔
562

563
    #[test]
564
    fn test_section_header_leading_whitespace() {
1✔
565
        let tokens = tokenize_gctf("  --- ENDPOINT ---");
1✔
566
        match &tokens[0].kind {
1✔
567
            GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "ENDPOINT"),
1✔
NEW
568
            _ => panic!("expected SectionHeader"),
×
569
        }
570
    }
1✔
571

572
    #[test]
573
    fn test_section_header_extra_whitespace() {
1✔
574
        let tokens = tokenize_gctf("---   RESPONSE   partial=true   ---");
1✔
575
        match &tokens[0].kind {
1✔
576
            GctfTokenKind::SectionHeader { name, raw_options } => {
1✔
577
                assert_eq!(name, "RESPONSE");
1✔
578
                assert_eq!(raw_options, "partial=true");
1✔
579
            }
NEW
580
            _ => panic!("expected SectionHeader"),
×
581
        }
582
    }
1✔
583

584
    #[test]
585
    fn test_section_header_lowercase_rejected() {
1✔
586
        let tokens = tokenize_gctf("--- endpoint ---");
1✔
587
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
588
    }
1✔
589

590
    #[test]
591
    fn test_section_header_mixed_case_treated_as_partial_name() {
1✔
592
        let tokens = tokenize_gctf("--- Endpoint ---");
1✔
593
        match &tokens[0].kind {
1✔
594
            GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "E"),
1✔
NEW
595
            _ => panic!("expected SectionHeader with truncated name"),
×
596
        }
597
    }
1✔
598

599
    #[test]
600
    fn test_section_header_fully_lowercase_rejected() {
1✔
601
        let tokens = tokenize_gctf("--- endpoint ---");
1✔
602
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
603
    }
1✔
604

605
    #[test]
606
    fn test_section_header_with_underscore() {
1✔
607
        let tokens = tokenize_gctf("--- REQUEST_HEADERS ---");
1✔
608
        match &tokens[0].kind {
1✔
609
            GctfTokenKind::SectionHeader { name, .. } => assert_eq!(name, "REQUEST_HEADERS"),
1✔
NEW
610
            _ => panic!("expected SectionHeader"),
×
611
        }
612
    }
1✔
613

614
    #[test]
615
    fn test_three_dashes_in_content() {
1✔
616
        let tokens = tokenize_gctf("---ABC");
1✔
617
        assert!(matches!(tokens[0].kind, GctfTokenKind::Content(_)));
1✔
618
    }
1✔
619

620
    #[test]
621
    fn test_comment_with_leading_whitespace() {
1✔
622
        let tokens = tokenize_gctf("  # indented comment");
1✔
623
        assert!(
1✔
624
            matches!(&tokens[0].kind, GctfTokenKind::Comment(t) if t == "  # indented comment")
1✔
625
        );
626
    }
1✔
627

628
    #[test]
629
    fn test_slash_slash_not_at_start_is_content() {
1✔
630
        let tokens = tokenize_gctf("foo // bar");
1✔
631
        assert!(matches!(&tokens[0].kind, GctfTokenKind::Content(t) if t == "foo // bar"));
1✔
632
    }
1✔
633

634
    #[test]
635
    fn test_tab_only_line_is_blank() {
1✔
636
        let tokens = tokenize_gctf("\t\t");
1✔
637
        assert!(matches!(tokens[0].kind, GctfTokenKind::Blank));
1✔
638
    }
1✔
639

640
    // === tokenize_kv_line edge cases ===
641

642
    #[test]
643
    fn test_kv_line_empty_value() {
1✔
644
        let (key, value) = tokenize_kv_line("key:").unwrap();
1✔
645
        assert_eq!(key, "key");
1✔
646
        assert_eq!(value, "");
1✔
647
    }
1✔
648

649
    #[test]
650
    fn test_kv_line_colon_in_value() {
1✔
651
        let (key, value) = tokenize_kv_line("url: http://host:8080").unwrap();
1✔
652
        assert_eq!(key, "url");
1✔
653
        assert_eq!(value, "http://host:8080");
1✔
654
    }
1✔
655

656
    #[test]
657
    fn test_kv_line_value_with_spaces() {
1✔
658
        let (key, value) = tokenize_kv_line("  cert  :  /path/to/cert.pem  ").unwrap();
1✔
659
        assert_eq!(key, "cert");
1✔
660
        assert_eq!(value, "/path/to/cert.pem");
1✔
661
    }
1✔
662

663
    #[test]
664
    fn test_kv_line_tab_separator() {
1✔
665
        let (key, value) = tokenize_kv_line("key\t:\tvalue").unwrap();
1✔
666
        assert_eq!(key, "key");
1✔
667
        assert_eq!(value, "value");
1✔
668
    }
1✔
669

670
    #[test]
671
    fn test_kv_line_only_whitespace_key_produces_empty_key() {
1✔
672
        let result = tokenize_kv_line("   : value");
1✔
673
        assert!(result.is_some());
1✔
674
        let (key, value) = result.unwrap();
1✔
675
        assert_eq!(key, "");
1✔
676
        assert_eq!(value, "value");
1✔
677
    }
1✔
678

679
    // === tokenize_extract_line edge cases ===
680

681
    #[test]
682
    fn test_extract_line_with_spaces() {
1✔
683
        let (name, value) = tokenize_extract_line("  total  =  .response.total  ").unwrap();
1✔
684
        assert_eq!(name, "total");
1✔
685
        assert_eq!(value, ".response.total");
1✔
686
    }
1✔
687

688
    #[test]
689
    fn test_extract_line_empty_value() {
1✔
690
        let (name, value) = tokenize_extract_line("name=").unwrap();
1✔
691
        assert_eq!(name, "name");
1✔
692
        assert_eq!(value, "");
1✔
693
    }
1✔
694

695
    #[test]
696
    fn test_extract_line_no_equals() {
1✔
697
        assert_eq!(tokenize_extract_line(".just.a.path"), None);
1✔
698
    }
1✔
699

700
    #[test]
701
    fn test_extract_line_whitespace_only() {
1✔
702
        assert_eq!(tokenize_extract_line("   "), None);
1✔
703
    }
1✔
704

705
    #[test]
706
    fn test_extract_line_only_whitespace_value() {
1✔
707
        let (name, value) = tokenize_extract_line("name=   ").unwrap();
1✔
708
        assert_eq!(name, "name");
1✔
709
        assert_eq!(value, "");
1✔
710
    }
1✔
711

712
    // === tokenize_inline_options edge cases ===
713

714
    #[test]
715
    fn test_tokenize_options_empty() {
1✔
716
        let opts = tokenize_inline_options("");
1✔
717
        assert!(opts.is_empty());
1✔
718
    }
1✔
719

720
    #[test]
721
    fn test_tokenize_options_only_spaces() {
1✔
722
        let opts = tokenize_inline_options("   ");
1✔
723
        assert!(opts.is_empty());
1✔
724
    }
1✔
725

726
    #[test]
727
    fn test_tokenize_options_escaped_char() {
1✔
728
        let opts = tokenize_inline_options(r#"key=va\"lue"#);
1✔
729
        assert_eq!(opts.len(), 1);
1✔
730
        assert_eq!(opts[0].0, "key");
1✔
731
    }
1✔
732

733
    #[test]
734
    fn test_tokenize_options_single_quotes_not_special() {
1✔
735
        let opts = tokenize_inline_options("key='hello world'");
1✔
736
        assert_eq!(opts.len(), 2);
1✔
737
        assert_eq!(opts[0], ("key".into(), "'hello".into()));
1✔
738
    }
1✔
739

740
    #[test]
741
    fn test_tokenize_options_array_value() {
1✔
742
        let opts = tokenize_inline_options(r#"redact=["field1","field2"]"#);
1✔
743
        assert_eq!(opts.len(), 1);
1✔
744
        assert_eq!(opts[0].0, "redact");
1✔
745
        assert!(opts[0].1.contains("field1"));
1✔
746
    }
1✔
747

748
    #[test]
749
    fn test_tokenize_options_multiple_spaces() {
1✔
750
        let opts = tokenize_inline_options("  a=1   b=2  ");
1✔
751
        assert_eq!(opts.len(), 2);
1✔
752
        assert_eq!(opts[0], ("a".into(), "1".into()));
1✔
753
        assert_eq!(opts[1], ("b".into(), "2".into()));
1✔
754
    }
1✔
755

756
    #[test]
757
    fn test_tokenize_options_tab_separated() {
1✔
758
        let opts = tokenize_inline_options("a=1\tb=2");
1✔
759
        assert_eq!(opts.len(), 2);
1✔
760
    }
1✔
761

762
    #[test]
763
    fn test_tokenize_options_quoted_key() {
1✔
764
        let opts = tokenize_inline_options(r#""key"=value"#);
1✔
765
        assert_eq!(opts.len(), 1);
1✔
766
        assert_eq!(opts[0].0, "key");
1✔
767
    }
1✔
768

769
    #[test]
770
    fn test_tokenize_options_empty_value() {
1✔
771
        let opts = tokenize_inline_options("key=");
1✔
772
        assert_eq!(opts.len(), 1);
1✔
773
        assert_eq!(opts[0], ("key".into(), "".into()));
1✔
774
    }
1✔
775

776
    // === slice_str edge case (via other functions) ===
777

778
    #[test]
779
    fn test_span_tracking() {
1✔
780
        let tokens = tokenize_gctf("# comment\n--- ENDPOINT ---\nhello");
1✔
781
        assert_eq!(tokens[0].span, Span { start: 0, end: 9 });
1✔
782
        assert_eq!(tokens[1].span, Span { start: 0, end: 16 });
1✔
783
        assert_eq!(tokens[2].span, Span { start: 0, end: 5 });
1✔
784
    }
1✔
785

786
    #[test]
787
    fn test_gctf_token_kind_equality() {
1✔
788
        let t1 = GctfToken {
1✔
789
            kind: GctfTokenKind::Blank,
1✔
790
            line: 0,
1✔
791
            span: Span { start: 0, end: 0 },
1✔
792
        };
1✔
793
        let t2 = GctfToken {
1✔
794
            kind: GctfTokenKind::Blank,
1✔
795
            line: 1,
1✔
796
            span: Span { start: 0, end: 0 },
1✔
797
        };
1✔
798
        assert_ne!(t1, t2); // different line
1✔
799
    }
1✔
800
}
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