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

gripmock / grpctestify-rust / 25340257197

04 May 2026 07:55PM UTC coverage: 78.205% (+0.2%) from 78.02%
25340257197

push

github

web-flow
Merge pull request #44 from gripmock/chore-gt

add attrs

954 of 1149 new or added lines in 23 files covered. (83.03%)

5 existing lines in 4 files now uncovered.

20435 of 26130 relevant lines covered (78.21%)

38120.8 hits per line

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

92.31
/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::{
8
    FileMeta, GctfAttribute, InlineOptions, Section, SectionContent, SectionType,
9
};
10
use crate::parser::gctf_tokenizer::{
11
    tokenize_extract_line, tokenize_inline_options, tokenize_kv_line,
12
};
13
use crate::parser::json_mod;
14
use crate::parser::json_stream_parser;
15

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

20
    if content.is_empty() {
132,595✔
21
        return Ok(SectionContent::Empty);
2✔
22
    }
132,593✔
23

24
    match section_type {
132,593✔
25
        // Single value sections
26
        SectionType::Address | SectionType::Endpoint => {
27
            Ok(SectionContent::Single(content.to_string()))
40,550✔
28
        }
29

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

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

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

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

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

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

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

101
    Ok(Section {
132,579✔
102
        section_type,
132,579✔
103
        content: section_content,
132,579✔
104
        inline_options,
132,579✔
105
        raw_content,
132,579✔
106
        start_line,
132,579✔
107
        end_line,
132,579✔
108
        attributes,
132,579✔
109
    })
132,579✔
110
}
132,579✔
111

112
/// Parse key=value options from section header inline options string.
113
pub fn parse_inline_options(s: &str) -> Result<InlineOptions> {
73✔
114
    let mut inline_options = InlineOptions::default();
73✔
115

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

145
    Ok(inline_options)
73✔
146
}
73✔
147

148
/// Parse a GCTF attribute from `#[name(value)]` content string.
149
/// Returns `None` if content is empty or invalid.
150
pub fn parse_attribute(content: &str) -> Option<GctfAttribute> {
19✔
151
    let content = content.trim();
19✔
152
    if content.is_empty() {
19✔
153
        return None;
2✔
154
    }
17✔
155

156
    let bytes = content.as_bytes();
17✔
157
    let len = bytes.len();
17✔
158
    let mut pos = 0;
17✔
159

160
    while pos < len && is_attr_name_char(bytes[pos]) {
112✔
161
        pos += 1;
95✔
162
    }
95✔
163

164
    if pos == 0 {
17✔
NEW
165
        return None;
×
166
    }
17✔
167

168
    let name = content[..pos].to_string();
17✔
169

170
    while pos < len && is_ws(bytes[pos]) {
17✔
NEW
171
        pos += 1;
×
NEW
172
    }
×
173

174
    if pos == len {
17✔
175
        return Some(GctfAttribute::flag(&name));
5✔
176
    }
12✔
177

178
    if bytes[pos] != b'(' {
12✔
NEW
179
        return None;
×
180
    }
12✔
181

182
    pos += 1;
12✔
183

184
    let value_start = pos;
12✔
185
    let mut paren_depth = 1;
12✔
186
    let mut escaped = false;
12✔
187

188
    while pos < len && paren_depth > 0 {
48✔
189
        if escaped {
36✔
NEW
190
            escaped = false;
×
NEW
191
            pos += 1;
×
NEW
192
            continue;
×
193
        }
36✔
194
        match bytes[pos] {
36✔
NEW
195
            b'\\' => {
×
NEW
196
                escaped = true;
×
NEW
197
                pos += 1;
×
NEW
198
            }
×
199
            b'"' | b'\'' => {
200
                let quote = bytes[pos];
1✔
201
                pos += 1;
1✔
202
                while pos < len {
12✔
203
                    if escaped {
12✔
NEW
204
                        escaped = false;
×
NEW
205
                        pos += 1;
×
NEW
206
                        continue;
×
207
                    }
12✔
208
                    if bytes[pos] == b'\\' {
12✔
NEW
209
                        escaped = true;
×
NEW
210
                        pos += 1;
×
NEW
211
                        continue;
×
212
                    }
12✔
213
                    if bytes[pos] == quote {
12✔
214
                        pos += 1;
1✔
215
                        break;
1✔
216
                    }
11✔
217
                    pos += 1;
11✔
218
                }
219
            }
NEW
220
            b'(' => {
×
NEW
221
                paren_depth += 1;
×
NEW
222
                pos += 1;
×
NEW
223
            }
×
224
            b')' => {
12✔
225
                paren_depth -= 1;
12✔
226
                pos += 1;
12✔
227
            }
12✔
228
            _ => pos += 1,
23✔
229
        }
230
    }
231

232
    if paren_depth != 0 {
12✔
NEW
233
        return None;
×
234
    }
12✔
235

236
    let value = content[value_start..pos - 1].to_string();
12✔
237
    Some(GctfAttribute::new(&name, &value))
12✔
238
}
19✔
239

240
fn is_attr_name_char(b: u8) -> bool {
107✔
241
    b.is_ascii_alphanumeric() || b == b'_' || b == b'-'
107✔
242
}
107✔
243

244
fn is_ws(b: u8) -> bool {
12✔
245
    b == b' ' || b == b'\t'
12✔
246
}
12✔
247

248
/// Resolve attributes for a section, applying inheritance rules:
249
/// - Attributes from parent sections apply to child sections
250
/// - Child section attributes override parent attributes
251
/// - Attributes with the same name are overridden (not merged)
252
pub fn resolve_attributes(
4✔
253
    section_attrs: &[GctfAttribute],
4✔
254
    inherited_attrs: &[GctfAttribute],
4✔
255
) -> Vec<GctfAttribute> {
4✔
256
    let mut resolved: Vec<GctfAttribute> = inherited_attrs.to_vec();
4✔
257
    let mut seen: std::collections::HashSet<String> =
4✔
258
        inherited_attrs.iter().map(|a| a.name.clone()).collect();
4✔
259

260
    for attr in section_attrs {
4✔
261
        if seen.contains(&attr.name) {
2✔
262
            let idx = resolved.iter().position(|a| a.name == attr.name).unwrap();
1✔
263
            resolved[idx] = attr.clone();
1✔
264
        } else {
1✔
265
            resolved.push(attr.clone());
1✔
266
            seen.insert(attr.name.clone());
1✔
267
        }
1✔
268
    }
269

270
    resolved
4✔
271
}
4✔
272

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

277
    for line in content.lines() {
61✔
278
        if let Some((key, value)) = tokenize_kv_line(line) {
61✔
279
            key_values.insert(key, value);
59✔
280
        }
59✔
281
    }
282

283
    Ok(key_values)
34✔
284
}
34✔
285

286
/// Parse assertions section (one assertion per line).
287
fn parse_assertions(content: &str) -> Result<Vec<String>> {
139✔
288
    use crate::parser::assertions::strip_assertion_comments;
289

290
    // No normalization needed — regex literals /pattern/ are now handled
291
    // by the assertion AST parser as Expr::RegExp nodes.
292
    let assertions: Vec<String> = content
139✔
293
        .lines()
139✔
294
        .filter_map(strip_assertion_comments)
139✔
295
        .collect();
139✔
296

297
    Ok(assertions)
139✔
298
}
139✔
299

300
#[cfg(test)]
301
mod tests {
302
    use super::*;
303

304
    #[test]
305
    fn test_tokenize_options() {
1✔
306
        let result = tokenize_inline_options("key1=value1 key2=value2");
1✔
307
        assert_eq!(result.len(), 2);
1✔
308
        assert_eq!(result[0], ("key1".into(), "value1".into()));
1✔
309
        assert_eq!(result[1], ("key2".into(), "value2".into()));
1✔
310
    }
1✔
311

312
    #[test]
313
    fn test_parse_inline_options() {
1✔
314
        let result = parse_inline_options("with_asserts=true partial=false tolerance=0.1").unwrap();
1✔
315
        assert!(result.with_asserts);
1✔
316
        assert!(!result.partial);
1✔
317
        assert_eq!(result.tolerance, Some(0.1));
1✔
318
    }
1✔
319

320
    #[test]
321
    fn test_parse_section_content_single_value() {
1✔
322
        let result = parse_section_content(SectionType::Address, "localhost:50051").unwrap();
1✔
323
        assert_eq!(
1✔
324
            result,
325
            SectionContent::Single("localhost:50051".to_string())
1✔
326
        );
327
    }
1✔
328

329
    #[test]
330
    fn test_parse_section_content_empty() {
1✔
331
        let result = parse_section_content(SectionType::Address, "").unwrap();
1✔
332
        assert_eq!(result, SectionContent::Empty);
1✔
333
    }
1✔
334

335
    #[test]
336
    fn test_parse_section_content_whitespace() {
1✔
337
        let result = parse_section_content(SectionType::Address, "   ").unwrap();
1✔
338
        assert_eq!(result, SectionContent::Empty);
1✔
339
    }
1✔
340

341
    #[test]
342
    fn test_parse_section_content_endpoint() {
1✔
343
        let result = parse_section_content(SectionType::Endpoint, "pkg.Service/Method").unwrap();
1✔
344
        assert_eq!(
1✔
345
            result,
346
            SectionContent::Single("pkg.Service/Method".to_string())
1✔
347
        );
348
    }
1✔
349

350
    #[test]
351
    fn test_parse_section_content_request_json() {
1✔
352
        let result = parse_section_content(SectionType::Request, r#"{"key": "value"}"#).unwrap();
1✔
353
        assert!(matches!(result, SectionContent::Json(_)));
1✔
354
    }
1✔
355

356
    #[test]
357
    fn test_parse_section_content_error_json() {
1✔
358
        let result = parse_section_content(SectionType::Error, r#"{"code": 5}"#).unwrap();
1✔
359
        assert!(matches!(result, SectionContent::Json(_)));
1✔
360
    }
1✔
361

362
    #[test]
363
    fn test_parse_section_content_response_json() {
1✔
364
        let result = parse_section_content(SectionType::Response, r#"{"status": "ok"}"#).unwrap();
1✔
365
        assert!(matches!(result, SectionContent::Json(_)));
1✔
366
    }
1✔
367

368
    #[test]
369
    fn test_parse_section_content_response_jsonlines() {
1✔
370
        let input = "{\"a\":1}\n{\"b\":2}";
1✔
371
        let result = parse_section_content(SectionType::Response, input).unwrap();
1✔
372
        assert!(matches!(result, SectionContent::JsonLines(v) if v.len() == 2));
1✔
373
    }
1✔
374

375
    #[test]
376
    fn test_parse_section_content_key_values() {
1✔
377
        let input = "ca_cert: /path/to/ca.pem\nserver_name: example.com";
1✔
378
        let result = parse_section_content(SectionType::Tls, input).unwrap();
1✔
379
        if let SectionContent::KeyValues(kv) = result {
1✔
380
            assert_eq!(kv.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1✔
381
            assert_eq!(kv.get("server_name"), Some(&"example.com".to_string()));
1✔
382
        } else {
383
            panic!("expected KeyValues");
×
384
        }
385
    }
1✔
386

387
    #[test]
388
    fn test_parse_section_content_key_values_with_comments() {
1✔
389
        let input = "# comment\nca_cert: /path/ca.pem\n\nkey: value";
1✔
390
        let result = parse_section_content(SectionType::Options, input).unwrap();
1✔
391
        if let SectionContent::KeyValues(kv) = result {
1✔
392
            assert_eq!(kv.len(), 2);
1✔
393
        } else {
394
            panic!("expected KeyValues");
×
395
        }
396
    }
1✔
397

398
    #[test]
399
    fn test_parse_section_content_extract() {
1✔
400
        let input = "total = .response.total\ncount = .items | length";
1✔
401
        let result = parse_section_content(SectionType::Extract, input).unwrap();
1✔
402
        if let SectionContent::Extract(kv) = result {
1✔
403
            assert_eq!(kv.get("total"), Some(&".response.total".to_string()));
1✔
404
            assert!(kv.contains_key("count"));
1✔
405
        } else {
406
            panic!("expected Extract");
×
407
        }
408
    }
1✔
409

410
    #[test]
411
    fn test_parse_section_content_extract_with_comments() {
1✔
412
        let input = "# ignore\n// ignore\ntotal = .response.total";
1✔
413
        let result = parse_section_content(SectionType::Extract, input).unwrap();
1✔
414
        if let SectionContent::Extract(kv) = result {
1✔
415
            assert_eq!(kv.len(), 1);
1✔
416
        } else {
417
            panic!("expected Extract");
×
418
        }
419
    }
1✔
420

421
    #[test]
422
    fn test_parse_section_content_asserts() {
1✔
423
        let input = ".x == 1\n.y != \"hello\"";
1✔
424
        let result = parse_section_content(SectionType::Asserts, input).unwrap();
1✔
425
        if let SectionContent::Assertions(asserts) = result {
1✔
426
            assert_eq!(asserts.len(), 2);
1✔
427
            assert_eq!(asserts[0], ".x == 1");
1✔
428
        } else {
429
            panic!("expected Assertions");
×
430
        }
431
    }
1✔
432

433
    #[test]
434
    fn test_parse_section_content_asserts_with_comments() {
1✔
435
        let input = ".x == 1 # inline\n# full line\n.y == 2 // comment";
1✔
436
        let result = parse_section_content(SectionType::Asserts, input).unwrap();
1✔
437
        if let SectionContent::Assertions(asserts) = result {
1✔
438
            assert_eq!(asserts.len(), 2);
1✔
439
        } else {
440
            panic!("expected Assertions");
×
441
        }
442
    }
1✔
443

444
    #[test]
445
    fn test_build_section() {
1✔
446
        let content = vec!["localhost:50051".to_string()];
1✔
447
        let section = build_section(
1✔
448
            SectionType::Address,
1✔
449
            5,
450
            6,
451
            &content,
1✔
452
            InlineOptions::default(),
1✔
453
            Vec::new(),
1✔
454
        )
455
        .unwrap();
1✔
456
        assert_eq!(section.section_type, SectionType::Address);
1✔
457
        assert_eq!(section.start_line, 5);
1✔
458
        assert_eq!(section.end_line, 6);
1✔
459
    }
1✔
460

461
    #[test]
462
    fn test_parse_inline_options_all_fields() {
1✔
463
        let result = parse_inline_options(
1✔
464
            "with_asserts=true partial=true tolerance=0.5 unordered_arrays=true",
1✔
465
        )
466
        .unwrap();
1✔
467
        assert!(result.with_asserts);
1✔
468
        assert!(result.partial);
1✔
469
        assert_eq!(result.tolerance, Some(0.5));
1✔
470
        assert!(result.unordered_arrays);
1✔
471
    }
1✔
472

473
    #[test]
474
    fn test_parse_inline_options_redact() {
1✔
475
        let result = parse_inline_options(r#"redact=["token","password"]"#).unwrap();
1✔
476
        assert_eq!(result.redact, vec!["token", "password"]);
1✔
477
    }
1✔
478

479
    #[test]
480
    fn test_parse_inline_options_empty() {
1✔
481
        let result = parse_inline_options("").unwrap();
1✔
482
        assert_eq!(result, InlineOptions::default());
1✔
483
    }
1✔
484

485
    #[test]
486
    fn test_parse_inline_options_unknown_key_ignored() {
1✔
487
        let result = parse_inline_options("unknown_key=value").unwrap();
1✔
488
        assert_eq!(result, InlineOptions::default());
1✔
489
    }
1✔
490

491
    #[test]
492
    fn test_parse_inline_options_tolerance_negative() {
1✔
493
        let result = parse_inline_options("tolerance=-0.5").unwrap();
1✔
494
        assert_eq!(result.tolerance, Some(-0.5));
1✔
495
    }
1✔
496

497
    #[test]
498
    fn test_parse_inline_options_tolerance_invalid() {
1✔
499
        let result = parse_inline_options("tolerance=not_a_number").unwrap();
1✔
500
        assert_eq!(result.tolerance, None);
1✔
501
    }
1✔
502

503
    #[test]
504
    fn test_parse_inline_options_redact_empty_array() {
1✔
505
        let result = parse_inline_options("redact=[]").unwrap();
1✔
506
        assert!(result.redact.is_empty());
1✔
507
    }
1✔
508

509
    #[test]
510
    fn test_parse_inline_options_redact_malformed() {
1✔
511
        let result = parse_inline_options("redact=not_an_array").unwrap();
1✔
512
        // Current tokenizer splits by spaces, so this becomes tokens
513
        // This is a known limitation - redact with spaces in value
514
        assert!(!result.redact.is_empty()); // tokenizer splits "not_an_array" into parts
1✔
515
    }
1✔
516

517
    #[test]
518
    fn test_parse_section_content_meta_full() {
1✔
519
        let result = parse_section_content(
1✔
520
            SectionType::Meta,
1✔
521
            r#"name: Test
1✔
522
summary: Summary
1✔
523
tags: [a, b]
1✔
524
owner: backend
1✔
525
links:
1✔
526
  - https://example.com
1✔
527
"#,
1✔
528
        )
529
        .unwrap();
1✔
530
        let SectionContent::Meta(m) = result else {
1✔
531
            panic!()
×
532
        };
533
        assert_eq!(m.name.as_deref(), Some("Test"));
1✔
534
        assert_eq!(m.summary.as_deref(), Some("Summary"));
1✔
535
        assert_eq!(m.tags, ["a", "b"]);
1✔
536
        assert_eq!(m.owner.as_deref(), Some("backend"));
1✔
537
        assert_eq!(m.links, ["https://example.com"]);
1✔
538
    }
1✔
539

540
    #[test]
541
    fn test_parse_section_content_meta_comments() {
1✔
542
        let result = parse_section_content(
1✔
543
            SectionType::Meta,
1✔
544
            r#"# comment
1✔
545
name: Test
1✔
546
tags: [a]
1✔
547
"#,
1✔
548
        )
549
        .unwrap();
1✔
550
        let SectionContent::Meta(m) = result else {
1✔
551
            panic!()
×
552
        };
553
        assert_eq!(m.name.as_deref(), Some("Test"));
1✔
554
        assert_eq!(m.tags, ["a"]);
1✔
555
    }
1✔
556

557
    #[test]
558
    fn test_parse_attribute_with_value() {
1✔
559
        let attr = parse_attribute("timeout(30)").unwrap();
1✔
560
        assert_eq!(attr.name, "timeout");
1✔
561
        assert_eq!(attr.value, "30");
1✔
562
        assert_eq!(attr.parse_u64(), Some(30));
1✔
563
    }
1✔
564

565
    #[test]
566
    fn test_parse_attribute_flag() {
1✔
567
        let attr = parse_attribute("skip").unwrap();
1✔
568
        assert_eq!(attr.name, "skip");
1✔
569
        assert_eq!(attr.value, "true");
1✔
570
        assert_eq!(attr.parse_bool(), Some(true));
1✔
571
    }
1✔
572

573
    #[test]
574
    fn test_parse_attribute_quoted_value() {
1✔
575
        let attr = parse_attribute(r#"tag("smoke, slow")"#).unwrap();
1✔
576
        assert_eq!(attr.name, "tag");
1✔
577
        assert_eq!(attr.value, r#""smoke, slow""#);
1✔
578
    }
1✔
579

580
    #[test]
581
    fn test_parse_attribute_with_spaces() {
1✔
582
        let attr = parse_attribute("  retry(3)  ").unwrap();
1✔
583
        assert_eq!(attr.name, "retry");
1✔
584
        assert_eq!(attr.value, "3");
1✔
585
    }
1✔
586

587
    #[test]
588
    fn test_parse_attribute_empty() {
1✔
589
        assert!(parse_attribute("").is_none());
1✔
590
        assert!(parse_attribute("   ").is_none());
1✔
591
    }
1✔
592

593
    #[test]
594
    fn test_parse_attribute_no_paren() {
1✔
595
        let attr = parse_attribute("just_a_name").unwrap();
1✔
596
        assert_eq!(attr.name, "just_a_name");
1✔
597
        assert_eq!(attr.value, "true");
1✔
598
    }
1✔
599

600
    #[test]
601
    fn test_resolve_attributes_inheritance() {
1✔
602
        let parent = vec![GctfAttribute::new("timeout", "10")];
1✔
603
        let child = vec![GctfAttribute::new("retry", "3")];
1✔
604
        let resolved = resolve_attributes(&child, &parent);
1✔
605

606
        let timeout = resolved.iter().find(|a| a.name == "timeout");
1✔
607
        let retry = resolved.iter().find(|a| a.name == "retry");
2✔
608

609
        assert_eq!(timeout.map(|a| a.value.as_str()), Some("10"));
1✔
610
        assert_eq!(retry.map(|a| a.value.as_str()), Some("3"));
1✔
611
    }
1✔
612

613
    #[test]
614
    fn test_resolve_attributes_override() {
1✔
615
        let parent = vec![GctfAttribute::new("timeout", "10")];
1✔
616
        let child = vec![GctfAttribute::new("timeout", "30")];
1✔
617
        let resolved = resolve_attributes(&child, &parent);
1✔
618

619
        assert_eq!(resolved.len(), 1);
1✔
620
        assert_eq!(resolved[0].value, "30");
1✔
621
    }
1✔
622

623
    #[test]
624
    fn test_resolve_attributes_empty() {
1✔
625
        let resolved = resolve_attributes(&[], &[]);
1✔
626
        assert!(resolved.is_empty());
1✔
627

628
        let parent = vec![GctfAttribute::new("timeout", "10")];
1✔
629
        let resolved = resolve_attributes(&[], &parent);
1✔
630
        assert_eq!(resolved.len(), 1);
1✔
631
        assert_eq!(resolved[0].value, "10");
1✔
632
    }
1✔
633
}
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