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

gripmock / grpctestify-rust / 24367988238

13 Apr 2026 09:32PM UTC coverage: 75.093% (-0.4%) from 75.445%
24367988238

Pull #35

github

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

2518 of 3593 new or added lines in 47 files covered. (70.08%)

155 existing lines in 9 files now uncovered.

16781 of 22347 relevant lines covered (75.09%)

2495.25 hits per line

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

96.72
/src/parser/ast.rs
1
// AST (Abstract Syntax Tree) for .gctf files
2
// Represents the parsed structure of a .gctf test file
3

4
use serde::{Deserialize, Serialize};
5
use std::collections::HashMap;
6

7
/// Complete .gctf document
8
/// Documents linked by `--- NEW ---` form a singly-linked list.
9
/// When `next_document` is `None` this is either a single-document file
10
/// or the last document in a chain.
11
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12
pub struct GctfDocument {
13
    /// File path (absolute or relative)
14
    pub file_path: String,
15

16
    /// All sections in the document (preserving order)
17
    pub sections: Vec<Section>,
18

19
    /// Document metadata
20
    pub metadata: DocumentMetadata,
21

22
    /// Next document in chain (linked by `--- NEW ---`), `None` if single/last
23
    #[serde(default, skip_serializing_if = "Option::is_none")]
24
    pub next_document: Option<Box<GctfDocument>>,
25
}
26

27
/// Document metadata
28
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29
pub struct DocumentMetadata {
30
    /// Original file content (for error reporting)
31
    pub source: Option<String>,
32

33
    /// File modification time (for caching)
34
    pub mtime: Option<i64>,
35

36
    /// Parsed at timestamp
37
    pub parsed_at: i64,
38
}
39

40
/// File-level metadata (META section)
41
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
42
#[serde(default)]
43
pub struct FileMeta {
44
    /// Test name
45
    #[serde(skip_serializing_if = "Option::is_none")]
46
    pub name: Option<String>,
47
    /// Test summary (one-liner)
48
    #[serde(skip_serializing_if = "Option::is_none")]
49
    pub summary: Option<String>,
50
    /// Test tags
51
    #[serde(skip_serializing_if = "Vec::is_empty")]
52
    pub tags: Vec<String>,
53
    /// Test owner (team/person)
54
    #[serde(skip_serializing_if = "Option::is_none")]
55
    pub owner: Option<String>,
56
    /// Related links (docs, jira, etc)
57
    #[serde(skip_serializing_if = "Vec::is_empty")]
58
    pub links: Vec<String>,
59
}
60

61
impl FileMeta {
62
    /// Check if meta has any content
NEW
63
    pub fn is_empty(&self) -> bool {
×
NEW
64
        self.name.is_none()
×
NEW
65
            && self.summary.is_none()
×
NEW
66
            && self.tags.is_empty()
×
NEW
67
            && self.owner.is_none()
×
NEW
68
            && self.links.is_empty()
×
NEW
69
    }
×
70
}
71

72
/// A section in the .gctf file
73
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74
pub struct Section {
75
    /// Section type
76
    pub section_type: SectionType,
77

78
    /// Content of the section (raw text, typically JSON)
79
    pub content: SectionContent,
80

81
    /// Inline options (for sections that support them)
82
    pub inline_options: InlineOptions,
83

84
    /// Raw text content of the section (preserved for formatting)
85
    pub raw_content: String,
86

87
    /// Line number where section starts
88
    pub start_line: usize,
89

90
    /// Line number where section ends
91
    pub end_line: usize,
92
}
93

94
/// Section content
95
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96
pub enum SectionContent {
97
    /// Single value (ADDRESS, ENDPOINT, etc.)
98
    Single(String),
99

100
    /// JSON object (REQUEST, RESPONSE, ERROR)
101
    Json(serde_json::Value),
102

103
    /// Newline-delimited JSON values within a single section block
104
    JsonLines(Vec<serde_json::Value>),
105

106
    /// Key-value pairs (REQUEST_HEADERS, TLS, OPTIONS, PROTO)
107
    KeyValues(HashMap<String, String>),
108

109
    /// Extract variables from response (EXTRACT)
110
    Extract(HashMap<String, String>),
111

112
    /// Assertion expressions (ASSERTS)
113
    Assertions(Vec<String>),
114

115
    /// File-level metadata (META)
116
    Meta(FileMeta),
117

118
    /// Empty section
119
    Empty,
120
}
121

122
/// Section types in .gctf files
123
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
124
pub enum SectionType {
125
    /// Server address
126
    Address,
127

128
    /// gRPC endpoint (service/method)
129
    Endpoint,
130

131
    /// Request payload (can have multiple)
132
    Request,
133

134
    /// Expected response (can have multiple)
135
    Response,
136

137
    /// Expected error
138
    Error,
139

140
    /// Request-specific headers
141
    RequestHeaders,
142

143
    /// Assertion expressions (can have multiple)
144
    Asserts,
145

146
    /// Protocol buffer configuration
147
    Proto,
148

149
    /// TLS/mTLS configuration
150
    Tls,
151

152
    /// Test execution options
153
    Options,
154

155
    /// Extract variables from response
156
    Extract,
157

158
    /// File-level metadata (suite, tags)
159
    Meta,
160
}
161

162
impl SectionType {
163
    /// Returns `true` if this section marks the end of a logical request-response cycle.
164
    pub fn is_terminal(&self) -> bool {
7✔
165
        matches!(
4✔
166
            self,
7✔
167
            SectionType::Response | SectionType::Error | SectionType::Asserts
168
        )
169
    }
7✔
170

171
    /// Get section name as string
172
    pub fn as_str(&self) -> &'static str {
505✔
173
        match self {
505✔
174
            SectionType::Address => "ADDRESS",
12✔
175
            SectionType::Endpoint => "ENDPOINT",
135✔
176
            SectionType::Request => "REQUEST",
145✔
177
            SectionType::Response => "RESPONSE",
144✔
178
            SectionType::Error => "ERROR",
9✔
179
            SectionType::RequestHeaders => "REQUEST_HEADERS",
10✔
180
            SectionType::Asserts => "ASSERTS",
30✔
181
            SectionType::Proto => "PROTO",
3✔
182
            SectionType::Tls => "TLS",
4✔
183
            SectionType::Options => "OPTIONS",
5✔
184
            SectionType::Extract => "EXTRACT",
8✔
NEW
185
            SectionType::Meta => "META",
×
186
        }
187
    }
505✔
188

189
    /// Parse section name string to SectionType
190
    pub fn from_keyword(s: &str) -> Option<SectionType> {
132,533✔
191
        match s.trim() {
132,533✔
192
            "ADDRESS" => Some(SectionType::Address),
132,533✔
193
            "ENDPOINT" => Some(SectionType::Endpoint),
132,514✔
194
            "REQUEST" => Some(SectionType::Request),
91,999✔
195
            "RESPONSE" => Some(SectionType::Response),
51,520✔
196
            "ERROR" => Some(SectionType::Error),
11,049✔
197
            "REQUEST_HEADERS" | "HEADERS" => Some(SectionType::RequestHeaders),
11,038✔
198
            "ASSERTS" => Some(SectionType::Asserts),
11,021✔
199
            "PROTO" => Some(SectionType::Proto),
10,889✔
200
            "TLS" => Some(SectionType::Tls),
10,884✔
201
            "OPTIONS" => Some(SectionType::Options),
10,878✔
202
            "EXTRACT" => Some(SectionType::Extract),
10,872✔
203
            "META" => Some(SectionType::Meta),
3✔
204
            _ => None,
3✔
205
        }
206
    }
132,533✔
207

208
    /// Check if section can appear multiple times
209
    pub fn is_multiple_allowed(&self) -> bool {
33,596✔
210
        matches!(
10,094✔
211
            self,
33,596✔
212
            SectionType::Request
213
                | SectionType::Response
214
                | SectionType::Asserts
215
                | SectionType::Extract
216
        )
217
    }
33,596✔
218

219
    /// Check if section is file-level (not inside documents)
NEW
220
    pub fn is_file_level(&self) -> bool {
×
NEW
221
        matches!(self, SectionType::Meta)
×
NEW
222
    }
×
223

224
    /// Check if section supports inline options
225
    pub fn supports_inline_options(&self) -> bool {
132,730✔
226
        matches!(self, SectionType::Response | SectionType::Error)
132,730✔
227
    }
132,730✔
228
}
229

230
/// Inline options for sections
231
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
232
pub struct InlineOptions {
233
    /// Run ASSERTS on same response (unary RPC)
234
    pub with_asserts: bool,
235

236
    /// Subset comparison (expected is subset of actual)
237
    pub partial: bool,
238

239
    /// Numeric tolerance for floating-point comparisons
240
    pub tolerance: Option<f64>,
241

242
    /// Remove sensitive fields before comparison
243
    pub redact: Vec<String>,
244

245
    /// Sort arrays for order-independent comparison
246
    pub unordered_arrays: bool,
247
}
248

249
impl InlineOptions {
250
    pub fn to_header_tokens(&self) -> Vec<String> {
63✔
251
        let mut parts = Vec::new();
63✔
252

253
        if self.partial {
63✔
254
            parts.push("partial".to_string());
11✔
255
        }
52✔
256

257
        if let Some(tolerance) = self.tolerance {
63✔
258
            parts.push(format!("tolerance={}", tolerance));
4✔
259
        }
59✔
260

261
        if !self.redact.is_empty() {
63✔
262
            let mut sorted_redact = self.redact.clone();
2✔
263
            sorted_redact.sort();
2✔
264
            let quoted = sorted_redact
2✔
265
                .iter()
2✔
266
                .map(|field| format!("\"{}\"", field))
3✔
267
                .collect::<Vec<_>>()
2✔
268
                .join(",");
2✔
269
            parts.push(format!("redact=[{}]", quoted));
2✔
270
        }
61✔
271

272
        if self.unordered_arrays {
63✔
273
            parts.push("unordered_arrays".to_string());
6✔
274
        }
57✔
275

276
        if self.with_asserts {
63✔
277
            parts.push("with_asserts".to_string());
13✔
278
        }
50✔
279

280
        parts
63✔
281
    }
63✔
282

283
    pub fn is_empty(&self) -> bool {
×
284
        !self.with_asserts
×
285
            && !self.partial
×
286
            && self.tolerance.is_none()
×
287
            && self.redact.is_empty()
×
288
            && !self.unordered_arrays
×
289
    }
×
290
}
291

292
impl Section {
293
    pub fn format_header(&self) -> String {
201✔
294
        let section = self.section_type.as_str();
201✔
295
        if self.section_type.supports_inline_options() {
201✔
296
            let parts = self.inline_options.to_header_tokens();
63✔
297
            if parts.is_empty() {
63✔
298
                format!("--- {} ---", section)
39✔
299
            } else {
300
                format!("--- {} {} ---", section, parts.join(" "))
24✔
301
            }
302
        } else {
303
            format!("--- {} ---", section)
138✔
304
        }
305
    }
201✔
306

307
    pub fn header_keyword_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
2✔
308
        let header_line = source.lines().nth(self.start_line)?.trim();
2✔
309
        let inner = header_line.strip_prefix("---")?.strip_suffix("---")?.trim();
2✔
310
        inner.split_whitespace().next()
2✔
311
    }
2✔
312
}
313

314
/// GCTF file header with inline options
315
/// Format: --- SECTION_NAME key=value ... ---
316
#[derive(Debug, Clone, PartialEq)]
317
pub struct SectionHeader {
318
    /// Section type
319
    pub section_type: SectionType,
320

321
    /// Inline options (key=value pairs)
322
    pub options: HashMap<String, String>,
323
}
324

325
/// Iterator over a linked document chain
326
pub struct DocumentChainIter<'a> {
327
    current: Option<&'a GctfDocument>,
328
}
329

330
impl<'a> Iterator for DocumentChainIter<'a> {
331
    type Item = &'a GctfDocument;
332

333
    fn next(&mut self) -> Option<Self::Item> {
1,251,553✔
334
        self.current.take().inspect(|doc| {
1,251,553✔
335
            self.current = doc.next_document.as_deref();
1,140,179✔
336
        })
1,140,179✔
337
    }
1,251,553✔
338
}
339

340
impl GctfDocument {
341
    /// Create a new empty document
342
    pub fn new(file_path: String) -> Self {
40,562✔
343
        Self {
40,562✔
344
            file_path,
40,562✔
345
            sections: Vec::new(),
40,562✔
346
            metadata: DocumentMetadata {
40,562✔
347
                source: None,
40,562✔
348
                mtime: None,
40,562✔
349
                parsed_at: crate::time::now_timestamp(),
40,562✔
350
            },
40,562✔
351
            next_document: None,
40,562✔
352
        }
40,562✔
353
    }
40,562✔
354

355
    /// Iterate over the document chain (single or `--- NEW ---` linked)
356
    pub fn iter_chain(&self) -> DocumentChainIter<'_> {
111,378✔
357
        DocumentChainIter {
111,378✔
358
            current: Some(self),
111,378✔
359
        }
111,378✔
360
    }
111,378✔
361

362
    /// Count documents in the chain
363
    pub fn document_count(&self) -> usize {
1,115✔
364
        self.iter_chain().count()
1,115✔
365
    }
1,115✔
366

367
    /// Check if this is a single-document file
368
    pub fn is_single_document(&self) -> bool {
10,005✔
369
        self.next_document.is_none()
10,005✔
370
    }
10,005✔
371

372
    /// Get document by index (0-based) from the chain
373
    pub fn get_document(&self, index: usize) -> Option<&GctfDocument> {
4✔
374
        self.iter_chain().nth(index)
4✔
375
    }
4✔
376

377
    /// Get all sections of a specific type
378
    pub fn sections_by_type(&self, section_type: SectionType) -> Vec<&Section> {
80,788✔
379
        self.sections
80,788✔
380
            .iter()
80,788✔
381
            .filter(|s| s.section_type == section_type)
269,945✔
382
            .collect()
80,788✔
383
    }
80,788✔
384

385
    /// Get first section of a specific type
386
    pub fn first_section(&self, section_type: SectionType) -> Option<&Section> {
100,939✔
387
        self.sections
100,939✔
388
            .iter()
100,939✔
389
            .find(|s| s.section_type == section_type)
259,441✔
390
    }
100,939✔
391

392
    /// Get address (from ADDRESS section or environment variable)
393
    pub fn get_address(&self, env_address: Option<&str>) -> Option<String> {
20,128✔
394
        if let Some(section) = self.first_section(SectionType::Address)
20,128✔
395
            && let SectionContent::Single(addr) = &section.content
40✔
396
        {
397
            return Some(addr.clone());
40✔
398
        }
20,088✔
399
        env_address.map(|s| s.to_string())
20,088✔
400
    }
20,128✔
401

402
    /// Get endpoint
403
    pub fn get_endpoint(&self) -> Option<String> {
20,207✔
404
        if let Some(section) = self.first_section(SectionType::Endpoint)
20,207✔
405
            && let SectionContent::Single(endpoint) = &section.content
20,204✔
406
        {
407
            return Some(endpoint.clone());
20,204✔
408
        }
3✔
409
        None
3✔
410
    }
20,207✔
411

412
    /// Parse endpoint into package, service, method
413
    pub fn parse_endpoint(&self) -> Option<(String, String, String)> {
78✔
414
        let endpoint = self.get_endpoint()?;
78✔
415
        let parts: Vec<&str> = endpoint.split('/').collect();
78✔
416
        if parts.len() == 2 {
78✔
417
            let full_service = parts[0];
77✔
418
            let service_parts: Vec<&str> = full_service.split('.').collect();
77✔
419
            if service_parts.len() >= 2 {
77✔
420
                let package = service_parts[..service_parts.len() - 1].join(".");
76✔
421
                let service = service_parts[service_parts.len() - 1].to_string();
76✔
422
                let method = parts[1].to_string();
76✔
423
                return Some((package, service, method));
76✔
424
            } else if service_parts.len() == 1 {
1✔
425
                let package = String::new();
1✔
426
                let service = service_parts[0].to_string();
1✔
427
                let method = parts[1].to_string();
1✔
428
                return Some((package, service, method));
1✔
429
            }
×
430
        }
1✔
431
        None
1✔
432
    }
78✔
433

434
    /// Get all request payloads
435
    pub fn get_requests(&self) -> Vec<serde_json::Value> {
2✔
436
        self.sections_by_type(SectionType::Request)
2✔
437
            .into_iter()
2✔
438
            .filter_map(|s| {
3✔
439
                if let SectionContent::Json(json) = &s.content {
3✔
440
                    Some(json.clone())
3✔
441
                } else {
442
                    None
×
443
                }
444
            })
3✔
445
            .collect()
2✔
446
    }
2✔
447

448
    /// Get all assertion sections
449
    pub fn get_assertions(&self) -> Vec<Vec<String>> {
2✔
450
        self.sections_by_type(SectionType::Asserts)
2✔
451
            .into_iter()
2✔
452
            .filter_map(|s| {
3✔
453
                if let SectionContent::Assertions(asserts) = &s.content {
3✔
454
                    Some(asserts.clone())
3✔
455
                } else {
456
                    None
×
457
                }
458
            })
3✔
459
            .collect()
2✔
460
    }
2✔
461

462
    /// Get request headers
463
    pub fn get_request_headers(&self) -> Option<HashMap<String, String>> {
3✔
464
        if let Some(section) = self.first_section(SectionType::RequestHeaders)
3✔
465
            && let SectionContent::KeyValues(headers) = &section.content
2✔
466
        {
467
            return Some(headers.clone());
2✔
468
        }
1✔
469
        None
1✔
470
    }
3✔
471

472
    /// Get TLS configuration
473
    pub fn get_tls_config(&self) -> Option<HashMap<String, String>> {
1✔
474
        if let Some(section) = self.first_section(SectionType::Tls)
1✔
475
            && let SectionContent::KeyValues(config) = &section.content
1✔
476
        {
477
            return Some(config.clone());
1✔
478
        }
×
479
        None
×
480
    }
1✔
481

482
    /// Get OPTIONS configuration
483
    pub fn get_options(&self) -> Option<HashMap<String, String>> {
4✔
484
        if let Some(section) = self.first_section(SectionType::Options)
4✔
485
            && let SectionContent::KeyValues(config) = &section.content
2✔
486
        {
487
            return Some(config.clone());
2✔
488
        }
2✔
489
        None
2✔
490
    }
4✔
491

492
    /// Get TLS configuration merged with defaults (section values override defaults)
493
    pub fn get_tls_config_with_defaults(
3✔
494
        &self,
3✔
495
        defaults: &HashMap<String, String>,
3✔
496
    ) -> Option<HashMap<String, String>> {
3✔
497
        let mut merged = defaults.clone();
3✔
498

499
        if let Some(section) = self.first_section(SectionType::Tls)
3✔
500
            && let SectionContent::KeyValues(config) = &section.content
1✔
501
        {
502
            for (key, value) in config {
1✔
503
                merged.insert(key.clone(), value.clone());
1✔
504
            }
1✔
505
        }
2✔
506

507
        if merged.is_empty() {
3✔
508
            None
1✔
509
        } else {
510
            Some(merged)
2✔
511
        }
512
    }
3✔
513

514
    /// Get PROTO configuration
515
    pub fn get_proto_config(&self) -> Option<HashMap<String, String>> {
2✔
516
        if let Some(section) = self.first_section(SectionType::Proto)
2✔
517
            && let SectionContent::KeyValues(config) = &section.content
1✔
518
        {
519
            return Some(config.clone());
1✔
520
        }
1✔
521
        None
1✔
522
    }
2✔
523

524
    /// Check for RESPONSE and ERROR conflict
525
    pub fn has_response_error_conflict(&self) -> bool {
10,064✔
526
        self.first_section(SectionType::Response).is_some()
10,064✔
527
            && self.first_section(SectionType::Error).is_some()
10,052✔
528
    }
10,064✔
529

530
    pub fn section_uses_deprecated_headers_alias(&self, section: &Section) -> bool {
13✔
531
        if section.section_type != SectionType::RequestHeaders {
13✔
532
            return false;
12✔
533
        }
1✔
534

535
        self.metadata
1✔
536
            .source
1✔
537
            .as_deref()
1✔
538
            .and_then(|source| section.header_keyword_from_source(source))
1✔
539
            .is_some_and(|keyword| keyword.eq_ignore_ascii_case("HEADERS"))
1✔
540
    }
13✔
541
}
542

543
#[cfg(test)]
544
mod tests {
545
    use super::*;
546
    use serde_json::json;
547

548
    #[test]
549
    fn test_section_type_from_str() {
1✔
550
        assert_eq!(
1✔
551
            SectionType::from_keyword("ADDRESS"),
1✔
552
            Some(SectionType::Address)
553
        );
554
        assert_eq!(
1✔
555
            SectionType::from_keyword("ENDPOINT"),
1✔
556
            Some(SectionType::Endpoint)
557
        );
558
        assert_eq!(SectionType::from_keyword("INVALID"), None);
1✔
559
    }
1✔
560

561
    #[test]
562
    fn test_section_type_multiple_allowed() {
1✔
563
        assert!(SectionType::Request.is_multiple_allowed());
1✔
564
        assert!(SectionType::Response.is_multiple_allowed());
1✔
565
        assert!(SectionType::Asserts.is_multiple_allowed());
1✔
566
        assert!(!SectionType::Address.is_multiple_allowed());
1✔
567
        assert!(!SectionType::Endpoint.is_multiple_allowed());
1✔
568
    }
1✔
569

570
    #[test]
571
    fn test_section_type_supports_inline_options() {
1✔
572
        assert!(SectionType::Response.supports_inline_options());
1✔
573
        assert!(SectionType::Error.supports_inline_options());
1✔
574
        assert!(!SectionType::Request.supports_inline_options());
1✔
575
        assert!(!SectionType::Address.supports_inline_options());
1✔
576
    }
1✔
577

578
    #[test]
579
    fn test_section_type_as_str() {
1✔
580
        assert_eq!(SectionType::Address.as_str(), "ADDRESS");
1✔
581
        assert_eq!(SectionType::Endpoint.as_str(), "ENDPOINT");
1✔
582
        assert_eq!(SectionType::Request.as_str(), "REQUEST");
1✔
583
        assert_eq!(SectionType::Response.as_str(), "RESPONSE");
1✔
584
        assert_eq!(SectionType::Error.as_str(), "ERROR");
1✔
585
        assert_eq!(SectionType::RequestHeaders.as_str(), "REQUEST_HEADERS");
1✔
586
        assert_eq!(SectionType::Asserts.as_str(), "ASSERTS");
1✔
587
        assert_eq!(SectionType::Proto.as_str(), "PROTO");
1✔
588
        assert_eq!(SectionType::Tls.as_str(), "TLS");
1✔
589
        assert_eq!(SectionType::Options.as_str(), "OPTIONS");
1✔
590
        assert_eq!(SectionType::Extract.as_str(), "EXTRACT");
1✔
591
    }
1✔
592

593
    #[test]
594
    fn test_section_type_from_keyword_aliases() {
1✔
595
        assert_eq!(
1✔
596
            SectionType::from_keyword("HEADERS"),
1✔
597
            Some(SectionType::RequestHeaders)
598
        );
599
        assert_eq!(
1✔
600
            SectionType::from_keyword("REQUEST_HEADERS"),
1✔
601
            Some(SectionType::RequestHeaders)
602
        );
603
    }
1✔
604

605
    #[test]
606
    fn test_section_type_from_keyword_case_insensitive() {
1✔
607
        // Should be case sensitive based on implementation
608
        assert_eq!(SectionType::from_keyword("address"), None);
1✔
609
        assert_eq!(
1✔
610
            SectionType::from_keyword("  ADDRESS  "),
1✔
611
            Some(SectionType::Address)
612
        );
613
    }
1✔
614

615
    #[test]
616
    fn test_section_type_is_terminal() {
1✔
617
        assert!(SectionType::Response.is_terminal());
1✔
618
        assert!(SectionType::Error.is_terminal());
1✔
619
        assert!(SectionType::Asserts.is_terminal());
1✔
620
        assert!(!SectionType::Request.is_terminal());
1✔
621
        assert!(!SectionType::Endpoint.is_terminal());
1✔
622
        assert!(!SectionType::Extract.is_terminal());
1✔
623
        assert!(!SectionType::Address.is_terminal());
1✔
624
    }
1✔
625

626
    #[test]
627
    fn test_gctf_document_new() {
1✔
628
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
629
        assert_eq!(doc.file_path, "test.gctf");
1✔
630
        assert!(doc.sections.is_empty());
1✔
631
        assert!(doc.metadata.source.is_none());
1✔
632
        assert!(doc.metadata.mtime.is_none());
1✔
633
    }
1✔
634

635
    #[test]
636
    fn test_gctf_document_sections_by_type() {
1✔
637
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
638
        doc.sections.push(Section {
1✔
639
            section_type: SectionType::Request,
1✔
640
            content: SectionContent::Json(json!({"key": "value1"})),
1✔
641
            inline_options: InlineOptions::default(),
1✔
642
            raw_content: "".to_string(),
1✔
643
            start_line: 1,
1✔
644
            end_line: 2,
1✔
645
        });
1✔
646
        doc.sections.push(Section {
1✔
647
            section_type: SectionType::Request,
1✔
648
            content: SectionContent::Json(json!({"key": "value2"})),
1✔
649
            inline_options: InlineOptions::default(),
1✔
650
            raw_content: "".to_string(),
1✔
651
            start_line: 3,
1✔
652
            end_line: 4,
1✔
653
        });
1✔
654
        doc.sections.push(Section {
1✔
655
            section_type: SectionType::Response,
1✔
656
            content: SectionContent::Json(json!({"result": "ok"})),
1✔
657
            inline_options: InlineOptions::default(),
1✔
658
            raw_content: "".to_string(),
1✔
659
            start_line: 5,
1✔
660
            end_line: 6,
1✔
661
        });
1✔
662

663
        let requests = doc.sections_by_type(SectionType::Request);
1✔
664
        assert_eq!(requests.len(), 2);
1✔
665

666
        let responses = doc.sections_by_type(SectionType::Response);
1✔
667
        assert_eq!(responses.len(), 1);
1✔
668

669
        let errors = doc.sections_by_type(SectionType::Error);
1✔
670
        assert_eq!(errors.len(), 0);
1✔
671
    }
1✔
672

673
    #[test]
674
    fn test_gctf_document_first_section() {
1✔
675
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
676
        doc.sections.push(Section {
1✔
677
            section_type: SectionType::Request,
1✔
678
            content: SectionContent::Json(json!({"key": "value"})),
1✔
679
            inline_options: InlineOptions::default(),
1✔
680
            raw_content: "".to_string(),
1✔
681
            start_line: 1,
1✔
682
            end_line: 2,
1✔
683
        });
1✔
684

685
        let first_request = doc.first_section(SectionType::Request);
1✔
686
        assert!(first_request.is_some());
1✔
687

688
        let first_error = doc.first_section(SectionType::Error);
1✔
689
        assert!(first_error.is_none());
1✔
690
    }
1✔
691

692
    #[test]
693
    fn test_gctf_document_get_address() {
1✔
694
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
695
        doc.sections.push(Section {
1✔
696
            section_type: SectionType::Address,
1✔
697
            content: SectionContent::Single("localhost:4770".to_string()),
1✔
698
            inline_options: InlineOptions::default(),
1✔
699
            raw_content: "".to_string(),
1✔
700
            start_line: 1,
1✔
701
            end_line: 1,
1✔
702
        });
1✔
703

704
        assert_eq!(doc.get_address(None), Some("localhost:4770".to_string()));
1✔
705
        assert_eq!(
1✔
706
            doc.get_address(Some("env:5000")),
1✔
707
            Some("localhost:4770".to_string())
1✔
708
        );
709

710
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
711
        assert_eq!(
1✔
712
            doc2.get_address(Some("env:5000")),
1✔
713
            Some("env:5000".to_string())
1✔
714
        );
715
        assert_eq!(doc2.get_address(None), None);
1✔
716
    }
1✔
717

718
    #[test]
719
    fn test_gctf_document_get_endpoint() {
1✔
720
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
721
        doc.sections.push(Section {
1✔
722
            section_type: SectionType::Endpoint,
1✔
723
            content: SectionContent::Single("my.Service/Method".to_string()),
1✔
724
            inline_options: InlineOptions::default(),
1✔
725
            raw_content: "".to_string(),
1✔
726
            start_line: 1,
1✔
727
            end_line: 1,
1✔
728
        });
1✔
729

730
        assert_eq!(doc.get_endpoint(), Some("my.Service/Method".to_string()));
1✔
731

732
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
733
        assert_eq!(doc2.get_endpoint(), None);
1✔
734
    }
1✔
735

736
    #[test]
737
    fn test_gctf_document_parse_endpoint() {
1✔
738
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
739
        doc.sections.push(Section {
1✔
740
            section_type: SectionType::Endpoint,
1✔
741
            content: SectionContent::Single("package.Service/Method".to_string()),
1✔
742
            inline_options: InlineOptions::default(),
1✔
743
            raw_content: "".to_string(),
1✔
744
            start_line: 1,
1✔
745
            end_line: 1,
1✔
746
        });
1✔
747

748
        let (package, service, method) = doc.parse_endpoint().unwrap();
1✔
749
        assert_eq!(package, "package");
1✔
750
        assert_eq!(service, "Service");
1✔
751
        assert_eq!(method, "Method");
1✔
752
    }
1✔
753

754
    #[test]
755
    fn test_gctf_document_parse_endpoint_no_package() {
1✔
756
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
757
        doc.sections.push(Section {
1✔
758
            section_type: SectionType::Endpoint,
1✔
759
            content: SectionContent::Single("Service/Method".to_string()),
1✔
760
            inline_options: InlineOptions::default(),
1✔
761
            raw_content: "".to_string(),
1✔
762
            start_line: 1,
1✔
763
            end_line: 1,
1✔
764
        });
1✔
765

766
        let (package, service, method) = doc.parse_endpoint().unwrap();
1✔
767
        assert_eq!(package, "");
1✔
768
        assert_eq!(service, "Service");
1✔
769
        assert_eq!(method, "Method");
1✔
770
    }
1✔
771

772
    #[test]
773
    fn test_gctf_document_parse_endpoint_invalid() {
1✔
774
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
775
        doc.sections.push(Section {
1✔
776
            section_type: SectionType::Endpoint,
1✔
777
            content: SectionContent::Single("invalid".to_string()),
1✔
778
            inline_options: InlineOptions::default(),
1✔
779
            raw_content: "".to_string(),
1✔
780
            start_line: 1,
1✔
781
            end_line: 1,
1✔
782
        });
1✔
783

784
        assert!(doc.parse_endpoint().is_none());
1✔
785
    }
1✔
786

787
    #[test]
788
    fn test_gctf_document_get_requests() {
1✔
789
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
790
        doc.sections.push(Section {
1✔
791
            section_type: SectionType::Request,
1✔
792
            content: SectionContent::Json(json!({"key": "value1"})),
1✔
793
            inline_options: InlineOptions::default(),
1✔
794
            raw_content: "".to_string(),
1✔
795
            start_line: 1,
1✔
796
            end_line: 2,
1✔
797
        });
1✔
798
        doc.sections.push(Section {
1✔
799
            section_type: SectionType::Request,
1✔
800
            content: SectionContent::Json(json!({"key": "value2"})),
1✔
801
            inline_options: InlineOptions::default(),
1✔
802
            raw_content: "".to_string(),
1✔
803
            start_line: 3,
1✔
804
            end_line: 4,
1✔
805
        });
1✔
806

807
        let requests = doc.get_requests();
1✔
808
        assert_eq!(requests.len(), 2);
1✔
809
        assert_eq!(requests[0], json!({"key": "value1"}));
1✔
810
        assert_eq!(requests[1], json!({"key": "value2"}));
1✔
811
    }
1✔
812

813
    #[test]
814
    fn test_gctf_document_get_assertions() {
1✔
815
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
816
        doc.sections.push(Section {
1✔
817
            section_type: SectionType::Asserts,
1✔
818
            content: SectionContent::Assertions(vec![".id == 1".to_string()]),
1✔
819
            inline_options: InlineOptions::default(),
1✔
820
            raw_content: "".to_string(),
1✔
821
            start_line: 1,
1✔
822
            end_line: 2,
1✔
823
        });
1✔
824
        doc.sections.push(Section {
1✔
825
            section_type: SectionType::Asserts,
1✔
826
            content: SectionContent::Assertions(vec![".name == \"test\"".to_string()]),
1✔
827
            inline_options: InlineOptions::default(),
1✔
828
            raw_content: "".to_string(),
1✔
829
            start_line: 3,
1✔
830
            end_line: 4,
1✔
831
        });
1✔
832

833
        let assertions = doc.get_assertions();
1✔
834
        assert_eq!(assertions.len(), 2);
1✔
835
        assert_eq!(assertions[0], vec![".id == 1"]);
1✔
836
        assert_eq!(assertions[1], vec![".name == \"test\""]);
1✔
837
    }
1✔
838

839
    #[test]
840
    fn test_gctf_document_get_request_headers() {
1✔
841
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
842
        let mut headers = HashMap::new();
1✔
843
        headers.insert("Authorization".to_string(), "Bearer token".to_string());
1✔
844
        doc.sections.push(Section {
1✔
845
            section_type: SectionType::RequestHeaders,
1✔
846
            content: SectionContent::KeyValues(headers.clone()),
1✔
847
            inline_options: InlineOptions::default(),
1✔
848
            raw_content: "".to_string(),
1✔
849
            start_line: 1,
1✔
850
            end_line: 2,
1✔
851
        });
1✔
852

853
        let result = doc.get_request_headers().unwrap();
1✔
854
        assert_eq!(
1✔
855
            result.get("Authorization"),
1✔
856
            Some(&"Bearer token".to_string())
1✔
857
        );
858
    }
1✔
859

860
    #[test]
861
    fn test_gctf_document_get_tls_config() {
1✔
862
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
863
        let mut config = HashMap::new();
1✔
864
        config.insert("ca_cert".to_string(), "/path/to/ca.pem".to_string());
1✔
865
        doc.sections.push(Section {
1✔
866
            section_type: SectionType::Tls,
1✔
867
            content: SectionContent::KeyValues(config.clone()),
1✔
868
            inline_options: InlineOptions::default(),
1✔
869
            raw_content: "".to_string(),
1✔
870
            start_line: 1,
1✔
871
            end_line: 2,
1✔
872
        });
1✔
873

874
        let result = doc.get_tls_config().unwrap();
1✔
875
        assert_eq!(result.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1✔
876
    }
1✔
877

878
    #[test]
879
    fn test_gctf_document_get_options() {
1✔
880
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
881
        let mut options = HashMap::new();
1✔
882
        options.insert("dry_run".to_string(), "true".to_string());
1✔
883
        options.insert("timeout".to_string(), "10".to_string());
1✔
884
        doc.sections.push(Section {
1✔
885
            section_type: SectionType::Options,
1✔
886
            content: SectionContent::KeyValues(options.clone()),
1✔
887
            inline_options: InlineOptions::default(),
1✔
888
            raw_content: "".to_string(),
1✔
889
            start_line: 1,
1✔
890
            end_line: 2,
1✔
891
        });
1✔
892

893
        let result = doc.get_options().unwrap();
1✔
894
        assert_eq!(result.get("dry_run"), Some(&"true".to_string()));
1✔
895
        assert_eq!(result.get("timeout"), Some(&"10".to_string()));
1✔
896
    }
1✔
897

898
    #[test]
899
    fn test_gctf_document_get_tls_config_with_defaults_env_only() {
1✔
900
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
901
        let mut defaults = HashMap::new();
1✔
902
        defaults.insert("server_name".to_string(), "example.com".to_string());
1✔
903

904
        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1✔
905
        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1✔
906
    }
1✔
907

908
    #[test]
909
    fn test_gctf_document_get_tls_config_with_defaults_section_overrides() {
1✔
910
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
911
        let mut config = HashMap::new();
1✔
912
        config.insert("insecure".to_string(), "true".to_string());
1✔
913
        doc.sections.push(Section {
1✔
914
            section_type: SectionType::Tls,
1✔
915
            content: SectionContent::KeyValues(config),
1✔
916
            inline_options: InlineOptions::default(),
1✔
917
            raw_content: "".to_string(),
1✔
918
            start_line: 1,
1✔
919
            end_line: 2,
1✔
920
        });
1✔
921

922
        let mut defaults = HashMap::new();
1✔
923
        defaults.insert("insecure".to_string(), "false".to_string());
1✔
924
        defaults.insert("server_name".to_string(), "example.com".to_string());
1✔
925

926
        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1✔
927
        assert_eq!(result.get("insecure"), Some(&"true".to_string()));
1✔
928
        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1✔
929
    }
1✔
930

931
    #[test]
932
    fn test_gctf_document_get_proto_config() {
1✔
933
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
934
        let mut config = HashMap::new();
1✔
935
        config.insert("files".to_string(), "service.proto".to_string());
1✔
936
        doc.sections.push(Section {
1✔
937
            section_type: SectionType::Proto,
1✔
938
            content: SectionContent::KeyValues(config.clone()),
1✔
939
            inline_options: InlineOptions::default(),
1✔
940
            raw_content: "".to_string(),
1✔
941
            start_line: 1,
1✔
942
            end_line: 2,
1✔
943
        });
1✔
944

945
        let result = doc.get_proto_config().unwrap();
1✔
946
        assert_eq!(result.get("files"), Some(&"service.proto".to_string()));
1✔
947
    }
1✔
948

949
    #[test]
950
    fn test_gctf_document_has_response_error_conflict() {
1✔
951
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
952
        assert!(!doc.has_response_error_conflict());
1✔
953

954
        doc.sections.push(Section {
1✔
955
            section_type: SectionType::Response,
1✔
956
            content: SectionContent::Json(json!({"result": "ok"})),
1✔
957
            inline_options: InlineOptions::default(),
1✔
958
            raw_content: "".to_string(),
1✔
959
            start_line: 1,
1✔
960
            end_line: 2,
1✔
961
        });
1✔
962
        assert!(!doc.has_response_error_conflict());
1✔
963

964
        doc.sections.push(Section {
1✔
965
            section_type: SectionType::Error,
1✔
966
            content: SectionContent::Json(json!({"code": 5})),
1✔
967
            inline_options: InlineOptions::default(),
1✔
968
            raw_content: "".to_string(),
1✔
969
            start_line: 3,
1✔
970
            end_line: 4,
1✔
971
        });
1✔
972
        assert!(doc.has_response_error_conflict());
1✔
973
    }
1✔
974

975
    #[test]
976
    fn test_inline_options_default() {
1✔
977
        let options = InlineOptions::default();
1✔
978
        assert!(!options.with_asserts);
1✔
979
        assert!(!options.partial);
1✔
980
        assert!(options.tolerance.is_none());
1✔
981
        assert!(options.redact.is_empty());
1✔
982
        assert!(!options.unordered_arrays);
1✔
983
    }
1✔
984

985
    #[test]
986
    fn test_section_format_header_with_inline_options() {
1✔
987
        let section = Section {
1✔
988
            section_type: SectionType::Response,
1✔
989
            content: SectionContent::Json(serde_json::json!({"ok": true})),
1✔
990
            inline_options: InlineOptions {
1✔
991
                with_asserts: true,
1✔
992
                partial: true,
1✔
993
                tolerance: Some(0.1),
1✔
994
                redact: vec!["token".to_string()],
1✔
995
                unordered_arrays: true,
1✔
996
            },
1✔
997
            raw_content: "".to_string(),
1✔
998
            start_line: 0,
1✔
999
            end_line: 0,
1✔
1000
        };
1✔
1001

1002
        let header = section.format_header();
1✔
1003
        assert_eq!(
1✔
1004
            header,
1005
            "--- RESPONSE partial tolerance=0.1 redact=[\"token\"] unordered_arrays with_asserts ---"
1006
        );
1007
    }
1✔
1008

1009
    #[test]
1010
    fn test_section_content_debug() {
1✔
1011
        let content = SectionContent::Single("test".to_string());
1✔
1012
        let debug_str = format!("{:?}", content);
1✔
1013
        assert!(debug_str.contains("Single"));
1✔
1014
    }
1✔
1015

1016
    #[test]
1017
    fn test_section_header_keyword_from_source() {
1✔
1018
        let section = Section {
1✔
1019
            section_type: SectionType::Response,
1✔
1020
            content: SectionContent::Json(serde_json::json!({"ok": true})),
1✔
1021
            inline_options: InlineOptions::default(),
1✔
1022
            raw_content: "{\"ok\":true}".to_string(),
1✔
1023
            start_line: 0,
1✔
1024
            end_line: 2,
1✔
1025
        };
1✔
1026

1027
        let source = "--- RESPONSE with_asserts=true ---\n{\"ok\":true}\n";
1✔
1028
        assert_eq!(section.header_keyword_from_source(source), Some("RESPONSE"));
1✔
1029
    }
1✔
1030

1031
    #[test]
1032
    fn test_document_detects_deprecated_headers_alias() {
1✔
1033
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
1034
        doc.metadata.source = Some("--- HEADERS ---\nAuthorization: Bearer t\n".to_string());
1✔
1035
        doc.sections.push(Section {
1✔
1036
            section_type: SectionType::RequestHeaders,
1✔
1037
            content: SectionContent::KeyValues(HashMap::from([(
1✔
1038
                "Authorization".to_string(),
1✔
1039
                "Bearer t".to_string(),
1✔
1040
            )])),
1✔
1041
            inline_options: InlineOptions::default(),
1✔
1042
            raw_content: "Authorization: Bearer t".to_string(),
1✔
1043
            start_line: 0,
1✔
1044
            end_line: 2,
1✔
1045
        });
1✔
1046

1047
        assert!(doc.section_uses_deprecated_headers_alias(&doc.sections[0]));
1✔
1048
    }
1✔
1049

1050
    #[test]
1051
    fn test_gctf_document_debug() {
1✔
1052
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1053
        let debug_str = format!("{:?}", doc);
1✔
1054
        assert!(debug_str.contains("test.gctf"));
1✔
1055
    }
1✔
1056

1057
    // ─── Document chain (linked-list) tests ───
1058

1059
    #[test]
1060
    fn test_document_chain_single() {
1✔
1061
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1062
        assert!(doc.is_single_document());
1✔
1063
        assert_eq!(doc.document_count(), 1);
1✔
1064
    }
1✔
1065

1066
    #[test]
1067
    fn test_document_chain_two_docs() {
1✔
1068
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1069
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1070
        doc1.next_document = Some(Box::new(doc2));
1✔
1071

1072
        assert!(!doc1.is_single_document());
1✔
1073
        assert_eq!(doc1.document_count(), 2);
1✔
1074
    }
1✔
1075

1076
    #[test]
1077
    fn test_document_chain_three_docs() {
1✔
1078
        let mut doc3 = GctfDocument::new("test.gctf".to_string());
1✔
1079
        doc3.file_path = "doc3".to_string();
1✔
1080

1081
        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1082
        doc2.file_path = "doc2".to_string();
1✔
1083
        doc2.next_document = Some(Box::new(doc3));
1✔
1084

1085
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1086
        doc1.file_path = "doc1".to_string();
1✔
1087
        doc1.next_document = Some(Box::new(doc2));
1✔
1088

1089
        assert_eq!(doc1.document_count(), 3);
1✔
1090

1091
        let docs: Vec<_> = doc1.iter_chain().collect();
1✔
1092
        assert_eq!(docs.len(), 3);
1✔
1093
        assert_eq!(docs[0].file_path, "doc1");
1✔
1094
        assert_eq!(docs[1].file_path, "doc2");
1✔
1095
        assert_eq!(docs[2].file_path, "doc3");
1✔
1096
    }
1✔
1097

1098
    #[test]
1099
    fn test_document_chain_get_document() {
1✔
1100
        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1101
        doc2.file_path = "doc2".to_string();
1✔
1102

1103
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1104
        doc1.file_path = "doc1".to_string();
1✔
1105
        doc1.next_document = Some(Box::new(doc2));
1✔
1106

1107
        assert_eq!(doc1.get_document(0).unwrap().file_path, "doc1");
1✔
1108
        assert_eq!(doc1.get_document(1).unwrap().file_path, "doc2");
1✔
1109
        assert!(doc1.get_document(2).is_none());
1✔
1110
    }
1✔
1111

1112
    #[test]
1113
    fn test_document_chain_iter_on_last() {
1✔
1114
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1115
        let docs: Vec<_> = doc.iter_chain().collect();
1✔
1116
        assert_eq!(docs.len(), 1);
1✔
1117
        assert_eq!(docs[0].file_path, "test.gctf");
1✔
1118
    }
1✔
1119
}
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