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

gripmock / grpctestify-rust / 25289586174

03 May 2026 08:11PM UTC coverage: 77.64% (-0.4%) from 78.02%
25289586174

Pull #44

github

web-flow
Merge b854536f8 into ee4f926ac
Pull Request #44: add attrs

546 of 813 new or added lines in 21 files covered. (67.16%)

5 existing lines in 4 files now uncovered.

20028 of 25796 relevant lines covered (77.64%)

38680.05 hits per line

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

92.46
/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
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9
pub struct GctfDocument {
10
    /// File path (absolute or relative)
11
    pub file_path: String,
12

13
    /// All sections in the document (preserving order)
14
    pub sections: Vec<Section>,
15

16
    /// Document metadata
17
    pub metadata: DocumentMetadata,
18

19
    /// Next document in chain (for multi-document files)
20
    #[serde(skip_serializing_if = "Option::is_none")]
21
    pub next_document: Option<Box<GctfDocument>>,
22
}
23

24
pub struct DocumentChainIter<'a> {
25
    current: Option<&'a GctfDocument>,
26
}
27

28
impl<'a> Iterator for DocumentChainIter<'a> {
29
    type Item = &'a GctfDocument;
30

31
    fn next(&mut self) -> Option<Self::Item> {
1,220,467✔
32
        let current = self.current?;
1,220,467✔
33
        self.current = current.next_document.as_deref();
1,110,194✔
34
        Some(current)
1,110,194✔
35
    }
1,220,467✔
36
}
37

38
/// Document metadata
39
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40
pub struct DocumentMetadata {
41
    /// Original file content (for error reporting)
42
    pub source: Option<String>,
43

44
    /// File modification time (for caching)
45
    pub mtime: Option<i64>,
46

47
    /// Parsed at timestamp
48
    pub parsed_at: i64,
49
}
50

51
impl Default for DocumentMetadata {
52
    fn default() -> Self {
40,575✔
53
        Self {
40,575✔
54
            source: None,
40,575✔
55
            mtime: None,
40,575✔
56
            parsed_at: crate::time::now_timestamp(),
40,575✔
57
        }
40,575✔
58
    }
40,575✔
59
}
60

61
/// File-level metadata (META section)
62
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
63
#[serde(default)]
64
pub struct FileMeta {
65
    /// Test name
66
    #[serde(skip_serializing_if = "Option::is_none")]
67
    pub name: Option<String>,
68
    /// Test summary (one-liner)
69
    #[serde(skip_serializing_if = "Option::is_none")]
70
    pub summary: Option<String>,
71
    /// Test tags
72
    #[serde(skip_serializing_if = "Vec::is_empty")]
73
    pub tags: Vec<String>,
74
    /// Test owner (team/person)
75
    #[serde(skip_serializing_if = "Option::is_none")]
76
    pub owner: Option<String>,
77
    /// Related links (docs, jira, etc)
78
    #[serde(skip_serializing_if = "Vec::is_empty")]
79
    pub links: Vec<String>,
80
}
81

82
impl FileMeta {
83
    /// Check if meta has any content
84
    pub fn is_empty(&self) -> bool {
×
85
        self.name.is_none()
×
86
            && self.summary.is_none()
×
87
            && self.tags.is_empty()
×
88
            && self.owner.is_none()
×
89
            && self.links.is_empty()
×
90
    }
×
91
}
92

93
/// GCTF attribute (#[name(value)] syntax)
94
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95
pub struct GctfAttribute {
96
    pub name: String,
97
    pub value: String,
98
}
99

100
impl GctfAttribute {
101
    pub fn new(name: &str, value: &str) -> Self {
8✔
102
        Self {
8✔
103
            name: name.to_string(),
8✔
104
            value: value.to_string(),
8✔
105
        }
8✔
106
    }
8✔
107

108
    pub fn flag(name: &str) -> Self {
2✔
109
        Self {
2✔
110
            name: name.to_string(),
2✔
111
            value: "true".to_string(),
2✔
112
        }
2✔
113
    }
2✔
114

115
    pub fn parse_u64(&self) -> Option<u64> {
1✔
116
        self.value.trim().parse::<u64>().ok()
1✔
117
    }
1✔
118

NEW
119
    pub fn parse_u32(&self) -> Option<u32> {
×
NEW
120
        self.value.trim().parse::<u32>().ok()
×
NEW
121
    }
×
122

NEW
123
    pub fn parse_f64(&self) -> Option<f64> {
×
NEW
124
        self.value.trim().parse::<f64>().ok()
×
NEW
125
    }
×
126

127
    pub fn parse_bool(&self) -> Option<bool> {
1✔
128
        match self.value.trim().to_lowercase().as_str() {
1✔
129
            "true" | "1" | "yes" | "on" => Some(true),
1✔
NEW
130
            "false" | "0" | "no" | "off" | "" => Some(false),
×
NEW
131
            _ => None,
×
132
        }
133
    }
1✔
134

NEW
135
    pub fn as_str(&self) -> &str {
×
NEW
136
        &self.value
×
NEW
137
    }
×
138
}
139

140
impl Section {
141
    pub fn get_attribute(&self, name: &str) -> Option<&GctfAttribute> {
9✔
142
        self.attributes.iter().find(|a| a.name == name)
9✔
143
    }
9✔
144

NEW
145
    pub fn get_timeout(&self) -> Option<u64> {
×
NEW
146
        self.get_attribute("timeout")
×
NEW
147
            .and_then(|a| a.parse_u64())
×
NEW
148
            .filter(|&v| v > 0)
×
NEW
149
    }
×
150

NEW
151
    pub fn get_retry(&self) -> Option<u32> {
×
NEW
152
        self.get_attribute("retry").and_then(|a| a.parse_u32())
×
NEW
153
    }
×
154

NEW
155
    pub fn get_skip(&self) -> bool {
×
NEW
156
        self.get_attribute("skip")
×
NEW
157
            .and_then(|a| a.parse_bool())
×
NEW
158
            .unwrap_or(false)
×
NEW
159
    }
×
160

NEW
161
    pub fn has_tag(&self, tag: &str) -> bool {
×
NEW
162
        self.get_attribute("tag")
×
NEW
163
            .map(|a| a.value.split(',').any(|t| t.trim() == tag))
×
NEW
164
            .unwrap_or(false)
×
NEW
165
    }
×
166
}
167

168
/// A section in the .gctf file
169
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170
pub struct Section {
171
    /// Section type
172
    pub section_type: SectionType,
173

174
    /// Content of the section (raw text, typically JSON)
175
    pub content: SectionContent,
176

177
    /// Inline options (for sections that support them)
178
    pub inline_options: InlineOptions,
179

180
    /// Raw text content of the section (preserved for formatting)
181
    pub raw_content: String,
182

183
    /// Line number where section starts
184
    pub start_line: usize,
185

186
    /// Line number where section ends
187
    pub end_line: usize,
188

189
    /// Local attributes for this section
190
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
191
    pub attributes: Vec<GctfAttribute>,
192
}
193

194
impl Default for Section {
NEW
195
    fn default() -> Self {
×
NEW
196
        Self {
×
NEW
197
            section_type: SectionType::Address,
×
NEW
198
            content: SectionContent::Empty,
×
NEW
199
            inline_options: InlineOptions::default(),
×
NEW
200
            raw_content: String::new(),
×
NEW
201
            start_line: 0,
×
NEW
202
            end_line: 0,
×
NEW
203
            attributes: Vec::new(),
×
NEW
204
        }
×
NEW
205
    }
×
206
}
207

208
/// Section content
209
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210
pub enum SectionContent {
211
    /// Single value (ADDRESS, ENDPOINT, etc.)
212
    Single(String),
213

214
    /// JSON object (REQUEST, RESPONSE, ERROR)
215
    Json(serde_json::Value),
216

217
    /// Newline-delimited JSON values within a single section block
218
    JsonLines(Vec<serde_json::Value>),
219

220
    /// Key-value pairs (REQUEST_HEADERS, TLS, OPTIONS, PROTO)
221
    KeyValues(HashMap<String, String>),
222

223
    /// Extract variables from response (EXTRACT)
224
    Extract(HashMap<String, String>),
225

226
    /// Assertion expressions (ASSERTS)
227
    Assertions(Vec<String>),
228

229
    /// File-level metadata (META)
230
    Meta(FileMeta),
231

232
    /// Empty section
233
    Empty,
234
}
235

236
/// Section types in .gctf files
237
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
238
pub enum SectionType {
239
    /// Server address
240
    Address,
241

242
    /// gRPC endpoint (service/method)
243
    Endpoint,
244

245
    /// Request payload (can have multiple)
246
    Request,
247

248
    /// Expected response (can have multiple)
249
    Response,
250

251
    /// Expected error
252
    Error,
253

254
    /// Request-specific headers
255
    RequestHeaders,
256

257
    /// Assertion expressions (can have multiple)
258
    Asserts,
259

260
    /// Protocol buffer configuration
261
    Proto,
262

263
    /// TLS/mTLS configuration
264
    Tls,
265

266
    /// Test execution options
267
    Options,
268

269
    /// Extract variables from response
270
    Extract,
271

272
    /// File-level metadata (suite, tags)
273
    Meta,
274
}
275

276
impl SectionType {
277
    /// Returns `true` if this section marks the end of a logical request-response cycle.
278
    pub fn is_terminal(&self) -> bool {
7✔
279
        matches!(
4✔
280
            self,
7✔
281
            SectionType::Response | SectionType::Error | SectionType::Asserts
282
        )
283
    }
7✔
284

285
    /// Get section name as string
286
    pub fn as_str(&self) -> &'static str {
554✔
287
        match self {
554✔
288
            SectionType::Address => "ADDRESS",
23✔
289
            SectionType::Endpoint => "ENDPOINT",
148✔
290
            SectionType::Request => "REQUEST",
158✔
291
            SectionType::Response => "RESPONSE",
146✔
292
            SectionType::Error => "ERROR",
12✔
293
            SectionType::RequestHeaders => "REQUEST_HEADERS",
13✔
294
            SectionType::Asserts => "ASSERTS",
31✔
295
            SectionType::Proto => "PROTO",
4✔
296
            SectionType::Tls => "TLS",
4✔
297
            SectionType::Options => "OPTIONS",
6✔
298
            SectionType::Extract => "EXTRACT",
8✔
299
            SectionType::Meta => "META",
1✔
300
        }
301
    }
554✔
302

303
    /// Parse section name string to SectionType
304
    pub fn from_keyword(s: &str) -> Option<SectionType> {
132,555✔
305
        match s.trim() {
132,555✔
306
            "ADDRESS" => Some(SectionType::Address),
132,555✔
307
            "ENDPOINT" => Some(SectionType::Endpoint),
132,536✔
308
            "REQUEST" => Some(SectionType::Request),
92,015✔
309
            "RESPONSE" => Some(SectionType::Response),
51,531✔
310
            "ERROR" => Some(SectionType::Error),
11,059✔
311
            "REQUEST_HEADERS" | "HEADERS" => Some(SectionType::RequestHeaders),
11,044✔
312
            "ASSERTS" => Some(SectionType::Asserts),
11,027✔
313
            "PROTO" => Some(SectionType::Proto),
10,890✔
314
            "TLS" => Some(SectionType::Tls),
10,885✔
315
            "OPTIONS" => Some(SectionType::Options),
10,879✔
316
            "EXTRACT" => Some(SectionType::Extract),
10,873✔
317
            "META" => Some(SectionType::Meta),
4✔
318
            _ => None,
3✔
319
        }
320
    }
132,555✔
321

322
    /// Check if section can appear multiple times
323
    pub fn is_multiple_allowed(&self) -> bool {
33,611✔
324
        matches!(
10,111✔
325
            self,
33,611✔
326
            SectionType::Request
327
                | SectionType::Response
328
                | SectionType::Asserts
329
                | SectionType::Extract
330
        )
331
    }
33,611✔
332

333
    /// Check if section is file-level (not inside documents)
334
    pub fn is_file_level(&self) -> bool {
×
335
        matches!(self, SectionType::Meta)
×
336
    }
×
337

338
    /// Check if section supports inline options
339
    pub fn supports_inline_options(&self) -> bool {
132,756✔
340
        matches!(self, SectionType::Response | SectionType::Error)
132,756✔
341
    }
132,756✔
342
}
343

344
/// Inline options for sections
345
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
346
pub struct InlineOptions {
347
    /// Run ASSERTS on same response (unary RPC)
348
    pub with_asserts: bool,
349

350
    /// Subset comparison (expected is subset of actual)
351
    pub partial: bool,
352

353
    /// Numeric tolerance for floating-point comparisons
354
    pub tolerance: Option<f64>,
355

356
    /// Remove sensitive fields before comparison
357
    pub redact: Vec<String>,
358

359
    /// Sort arrays for order-independent comparison
360
    pub unordered_arrays: bool,
361
}
362

363
impl InlineOptions {
364
    pub fn to_header_tokens(&self) -> Vec<String> {
64✔
365
        let mut parts = Vec::new();
64✔
366

367
        if self.partial {
64✔
368
            parts.push("partial".to_string());
12✔
369
        }
52✔
370

371
        if let Some(tolerance) = self.tolerance {
64✔
372
            parts.push(format!("tolerance={}", tolerance));
4✔
373
        }
60✔
374

375
        if !self.redact.is_empty() {
64✔
376
            let mut sorted_redact = self.redact.clone();
2✔
377
            sorted_redact.sort();
2✔
378
            let quoted = sorted_redact
2✔
379
                .iter()
2✔
380
                .map(|field| format!("\"{}\"", field))
3✔
381
                .collect::<Vec<_>>()
2✔
382
                .join(",");
2✔
383
            parts.push(format!("redact=[{}]", quoted));
2✔
384
        }
62✔
385

386
        if self.unordered_arrays {
64✔
387
            parts.push("unordered_arrays".to_string());
6✔
388
        }
58✔
389

390
        if self.with_asserts {
64✔
391
            parts.push("with_asserts".to_string());
14✔
392
        }
50✔
393

394
        parts
64✔
395
    }
64✔
396

397
    pub fn is_empty(&self) -> bool {
×
398
        !self.with_asserts
×
399
            && !self.partial
×
400
            && self.tolerance.is_none()
×
401
            && self.redact.is_empty()
×
402
            && !self.unordered_arrays
×
403
    }
×
404
}
405

406
impl Section {
407
    pub fn format_header(&self) -> String {
205✔
408
        let section = self.section_type.as_str();
205✔
409
        if self.section_type.supports_inline_options() {
205✔
410
            let parts = self.inline_options.to_header_tokens();
64✔
411
            if parts.is_empty() {
64✔
412
                format!("--- {} ---", section)
39✔
413
            } else {
414
                format!("--- {} {} ---", section, parts.join(" "))
25✔
415
            }
416
        } else {
417
            format!("--- {} ---", section)
141✔
418
        }
419
    }
205✔
420

421
    pub fn header_keyword_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
2✔
422
        let header_line = source.lines().nth(self.start_line)?.trim();
2✔
423
        let inner = header_line.strip_prefix("---")?.strip_suffix("---")?.trim();
2✔
424
        inner.split_whitespace().next()
2✔
425
    }
2✔
426
}
427

428
/// GCTF file header with inline options
429
/// Format: --- SECTION_NAME key=value ... ---
430
#[derive(Debug, Clone, PartialEq)]
431
pub struct SectionHeader {
432
    /// Section type
433
    pub section_type: SectionType,
434

435
    /// Inline options (key=value pairs)
436
    pub options: HashMap<String, String>,
437
}
438

439
impl GctfDocument {
440
    /// Create a new empty document
441
    pub fn new(file_path: String) -> Self {
40,575✔
442
        Self {
40,575✔
443
            file_path,
40,575✔
444
            sections: Vec::new(),
40,575✔
445
            metadata: DocumentMetadata::default(),
40,575✔
446
            next_document: None,
40,575✔
447
        }
40,575✔
448
    }
40,575✔
449

450
    /// Get document by index (0-based) from the chain
451
    pub fn get_document(&self, index: usize) -> Option<&GctfDocument> {
4✔
452
        self.iter_chain().nth(index)
4✔
453
    }
4✔
454

455
    pub fn iter_chain(&self) -> DocumentChainIter<'_> {
110,284✔
456
        DocumentChainIter {
110,284✔
457
            current: Some(self),
110,284✔
458
        }
110,284✔
459
    }
110,284✔
460

461
    pub fn document_count(&self) -> usize {
1,118✔
462
        let mut count = 1;
1,118✔
463
        let mut current = &self.next_document;
1,118✔
464
        while let Some(doc) = current {
30,013✔
465
            count += 1;
28,895✔
466
            current = &doc.next_document;
28,895✔
467
        }
28,895✔
468
        count
1,118✔
469
    }
1,118✔
470

471
    pub fn is_single_document(&self) -> bool {
10,005✔
472
        self.next_document.is_none()
10,005✔
473
    }
10,005✔
474

475
    /// Get all sections of a specific type
476
    pub fn sections_by_type(&self, section_type: SectionType) -> Vec<&Section> {
80,842✔
477
        self.sections
80,842✔
478
            .iter()
80,842✔
479
            .filter(|s| s.section_type == section_type)
270,170✔
480
            .collect()
80,842✔
481
    }
80,842✔
482

483
    /// Get first section of a specific type
484
    pub fn first_section(&self, section_type: SectionType) -> Option<&Section> {
101,070✔
485
        self.sections
101,070✔
486
            .iter()
101,070✔
487
            .find(|s| s.section_type == section_type)
259,773✔
488
    }
101,070✔
489

490
    /// Get address (from ADDRESS section or environment variable)
491
    pub fn get_address(&self, env_address: Option<&str>) -> Option<String> {
20,150✔
492
        if let Some(section) = self.first_section(SectionType::Address)
20,150✔
493
            && let SectionContent::Single(addr) = &section.content
65✔
494
        {
495
            return Some(addr.clone());
65✔
496
        }
20,085✔
497
        env_address.map(|s| s.to_string())
20,085✔
498
    }
20,150✔
499

500
    /// Get endpoint
501
    pub fn get_endpoint(&self) -> Option<String> {
20,234✔
502
        if let Some(section) = self.first_section(SectionType::Endpoint)
20,234✔
503
            && let SectionContent::Single(endpoint) = &section.content
20,231✔
504
        {
505
            return Some(endpoint.clone());
20,231✔
506
        }
3✔
507
        None
3✔
508
    }
20,234✔
509

510
    /// Parse endpoint into package, service, method
511
    pub fn parse_endpoint(&self) -> Option<(String, String, String)> {
90✔
512
        let endpoint = self.get_endpoint()?;
90✔
513
        let parts: Vec<&str> = endpoint.split('/').collect();
90✔
514
        if parts.len() == 2 {
90✔
515
            let full_service = parts[0];
89✔
516
            let service_parts: Vec<&str> = full_service.split('.').collect();
89✔
517
            if service_parts.len() >= 2 {
89✔
518
                let package = service_parts[..service_parts.len() - 1].join(".");
88✔
519
                let service = service_parts[service_parts.len() - 1].to_string();
88✔
520
                let method = parts[1].to_string();
88✔
521
                return Some((package, service, method));
88✔
522
            } else if service_parts.len() == 1 {
1✔
523
                let package = String::new();
1✔
524
                let service = service_parts[0].to_string();
1✔
525
                let method = parts[1].to_string();
1✔
526
                return Some((package, service, method));
1✔
527
            }
×
528
        }
1✔
529
        None
1✔
530
    }
90✔
531

532
    /// Get all request payloads
533
    pub fn get_requests(&self) -> Vec<serde_json::Value> {
11✔
534
        self.sections_by_type(SectionType::Request)
11✔
535
            .into_iter()
11✔
536
            .filter_map(|s| {
12✔
537
                if let SectionContent::Json(json) = &s.content {
12✔
538
                    Some(json.clone())
12✔
539
                } else {
540
                    None
×
541
                }
542
            })
12✔
543
            .collect()
11✔
544
    }
11✔
545

546
    /// Get all assertion sections
547
    pub fn get_assertions(&self) -> Vec<Vec<String>> {
2✔
548
        self.sections_by_type(SectionType::Asserts)
2✔
549
            .into_iter()
2✔
550
            .filter_map(|s| {
3✔
551
                if let SectionContent::Assertions(asserts) = &s.content {
3✔
552
                    Some(asserts.clone())
3✔
553
                } else {
554
                    None
×
555
                }
556
            })
3✔
557
            .collect()
2✔
558
    }
2✔
559

560
    /// Get request headers
561
    pub fn get_request_headers(&self) -> Option<HashMap<String, String>> {
22✔
562
        if let Some(section) = self.first_section(SectionType::RequestHeaders)
22✔
563
            && let SectionContent::KeyValues(headers) = &section.content
3✔
564
        {
565
            return Some(headers.clone());
3✔
566
        }
19✔
567
        None
19✔
568
    }
22✔
569

570
    /// Get TLS configuration
571
    pub fn get_tls_config(&self) -> Option<HashMap<String, String>> {
17✔
572
        if let Some(section) = self.first_section(SectionType::Tls)
17✔
573
            && let SectionContent::KeyValues(config) = &section.content
3✔
574
        {
575
            return Some(config.clone());
3✔
576
        }
14✔
577
        None
14✔
578
    }
17✔
579

580
    /// Get OPTIONS configuration
581
    pub fn get_options(&self) -> Option<HashMap<String, String>> {
13✔
582
        if let Some(section) = self.first_section(SectionType::Options)
13✔
583
            && let SectionContent::KeyValues(config) = &section.content
3✔
584
        {
585
            return Some(config.clone());
3✔
586
        }
10✔
587
        None
10✔
588
    }
13✔
589

590
    /// Get TLS configuration merged with defaults (section values override defaults)
591
    pub fn get_tls_config_with_defaults(
3✔
592
        &self,
3✔
593
        defaults: &HashMap<String, String>,
3✔
594
    ) -> Option<HashMap<String, String>> {
3✔
595
        let mut merged = defaults.clone();
3✔
596

597
        if let Some(section) = self.first_section(SectionType::Tls)
3✔
598
            && let SectionContent::KeyValues(config) = &section.content
1✔
599
        {
600
            for (key, value) in config {
1✔
601
                merged.insert(key.clone(), value.clone());
1✔
602
            }
1✔
603
        }
2✔
604

605
        if merged.is_empty() {
3✔
606
            None
1✔
607
        } else {
608
            Some(merged)
2✔
609
        }
610
    }
3✔
611

612
    /// Get PROTO configuration
613
    pub fn get_proto_config(&self) -> Option<HashMap<String, String>> {
11✔
614
        if let Some(section) = self.first_section(SectionType::Proto)
11✔
615
            && let SectionContent::KeyValues(config) = &section.content
3✔
616
        {
617
            return Some(config.clone());
3✔
618
        }
8✔
619
        None
8✔
620
    }
11✔
621

622
    /// Check for RESPONSE and ERROR conflict
623
    pub fn has_response_error_conflict(&self) -> bool {
10,067✔
624
        self.first_section(SectionType::Response).is_some()
10,067✔
625
            && self.first_section(SectionType::Error).is_some()
10,049✔
626
    }
10,067✔
627

628
    pub fn section_uses_deprecated_headers_alias(&self, section: &Section) -> bool {
13✔
629
        if section.section_type != SectionType::RequestHeaders {
13✔
630
            return false;
12✔
631
        }
1✔
632

633
        self.metadata
1✔
634
            .source
1✔
635
            .as_deref()
1✔
636
            .and_then(|source| section.header_keyword_from_source(source))
1✔
637
            .is_some_and(|keyword| keyword.eq_ignore_ascii_case("HEADERS"))
1✔
638
    }
13✔
639
}
640

641
#[cfg(test)]
642
mod tests {
643
    use super::*;
644
    use serde_json::json;
645

646
    #[test]
647
    fn test_section_type_from_str() {
1✔
648
        assert_eq!(
1✔
649
            SectionType::from_keyword("ADDRESS"),
1✔
650
            Some(SectionType::Address)
651
        );
652
        assert_eq!(
1✔
653
            SectionType::from_keyword("ENDPOINT"),
1✔
654
            Some(SectionType::Endpoint)
655
        );
656
        assert_eq!(SectionType::from_keyword("INVALID"), None);
1✔
657
    }
1✔
658

659
    #[test]
660
    fn test_section_type_multiple_allowed() {
1✔
661
        assert!(SectionType::Request.is_multiple_allowed());
1✔
662
        assert!(SectionType::Response.is_multiple_allowed());
1✔
663
        assert!(SectionType::Asserts.is_multiple_allowed());
1✔
664
        assert!(!SectionType::Address.is_multiple_allowed());
1✔
665
        assert!(!SectionType::Endpoint.is_multiple_allowed());
1✔
666
    }
1✔
667

668
    #[test]
669
    fn test_section_type_supports_inline_options() {
1✔
670
        assert!(SectionType::Response.supports_inline_options());
1✔
671
        assert!(SectionType::Error.supports_inline_options());
1✔
672
        assert!(!SectionType::Request.supports_inline_options());
1✔
673
        assert!(!SectionType::Address.supports_inline_options());
1✔
674
    }
1✔
675

676
    #[test]
677
    fn test_section_type_as_str() {
1✔
678
        assert_eq!(SectionType::Address.as_str(), "ADDRESS");
1✔
679
        assert_eq!(SectionType::Endpoint.as_str(), "ENDPOINT");
1✔
680
        assert_eq!(SectionType::Request.as_str(), "REQUEST");
1✔
681
        assert_eq!(SectionType::Response.as_str(), "RESPONSE");
1✔
682
        assert_eq!(SectionType::Error.as_str(), "ERROR");
1✔
683
        assert_eq!(SectionType::RequestHeaders.as_str(), "REQUEST_HEADERS");
1✔
684
        assert_eq!(SectionType::Asserts.as_str(), "ASSERTS");
1✔
685
        assert_eq!(SectionType::Proto.as_str(), "PROTO");
1✔
686
        assert_eq!(SectionType::Tls.as_str(), "TLS");
1✔
687
        assert_eq!(SectionType::Options.as_str(), "OPTIONS");
1✔
688
        assert_eq!(SectionType::Extract.as_str(), "EXTRACT");
1✔
689
    }
1✔
690

691
    #[test]
692
    fn test_section_type_from_keyword_aliases() {
1✔
693
        assert_eq!(
1✔
694
            SectionType::from_keyword("HEADERS"),
1✔
695
            Some(SectionType::RequestHeaders)
696
        );
697
        assert_eq!(
1✔
698
            SectionType::from_keyword("REQUEST_HEADERS"),
1✔
699
            Some(SectionType::RequestHeaders)
700
        );
701
    }
1✔
702

703
    #[test]
704
    fn test_section_type_from_keyword_case_insensitive() {
1✔
705
        // Should be case sensitive based on implementation
706
        assert_eq!(SectionType::from_keyword("address"), None);
1✔
707
        assert_eq!(
1✔
708
            SectionType::from_keyword("  ADDRESS  "),
1✔
709
            Some(SectionType::Address)
710
        );
711
    }
1✔
712

713
    #[test]
714
    fn test_section_type_is_terminal() {
1✔
715
        assert!(SectionType::Response.is_terminal());
1✔
716
        assert!(SectionType::Error.is_terminal());
1✔
717
        assert!(SectionType::Asserts.is_terminal());
1✔
718
        assert!(!SectionType::Request.is_terminal());
1✔
719
        assert!(!SectionType::Endpoint.is_terminal());
1✔
720
        assert!(!SectionType::Extract.is_terminal());
1✔
721
        assert!(!SectionType::Address.is_terminal());
1✔
722
    }
1✔
723

724
    #[test]
725
    fn test_gctf_document_new() {
1✔
726
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
727
        assert_eq!(doc.file_path, "test.gctf");
1✔
728
        assert!(doc.sections.is_empty());
1✔
729
        assert!(doc.metadata.source.is_none());
1✔
730
        assert!(doc.metadata.mtime.is_none());
1✔
731
    }
1✔
732

733
    #[test]
734
    fn test_gctf_document_sections_by_type() {
1✔
735
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
736
        doc.sections.push(Section {
1✔
737
            section_type: SectionType::Request,
1✔
738
            content: SectionContent::Json(json!({"key": "value1"})),
1✔
739
            inline_options: InlineOptions::default(),
1✔
740
            raw_content: "".to_string(),
1✔
741
            start_line: 1,
1✔
742
            end_line: 2,
1✔
743
            attributes: Vec::new(),
1✔
744
        });
1✔
745
        doc.sections.push(Section {
1✔
746
            section_type: SectionType::Request,
1✔
747
            content: SectionContent::Json(json!({"key": "value2"})),
1✔
748
            inline_options: InlineOptions::default(),
1✔
749
            raw_content: "".to_string(),
1✔
750
            start_line: 3,
1✔
751
            end_line: 4,
1✔
752
            attributes: Vec::new(),
1✔
753
        });
1✔
754
        doc.sections.push(Section {
1✔
755
            section_type: SectionType::Response,
1✔
756
            content: SectionContent::Json(json!({"result": "ok"})),
1✔
757
            inline_options: InlineOptions::default(),
1✔
758
            raw_content: "".to_string(),
1✔
759
            start_line: 5,
1✔
760
            end_line: 6,
1✔
761
            attributes: Vec::new(),
1✔
762
        });
1✔
763

764
        let requests = doc.sections_by_type(SectionType::Request);
1✔
765
        assert_eq!(requests.len(), 2);
1✔
766

767
        let responses = doc.sections_by_type(SectionType::Response);
1✔
768
        assert_eq!(responses.len(), 1);
1✔
769

770
        let errors = doc.sections_by_type(SectionType::Error);
1✔
771
        assert_eq!(errors.len(), 0);
1✔
772
    }
1✔
773

774
    #[test]
775
    fn test_gctf_document_first_section() {
1✔
776
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
777
        doc.sections.push(Section {
1✔
778
            section_type: SectionType::Request,
1✔
779
            content: SectionContent::Json(json!({"key": "value"})),
1✔
780
            inline_options: InlineOptions::default(),
1✔
781
            raw_content: "".to_string(),
1✔
782
            start_line: 1,
1✔
783
            end_line: 2,
1✔
784
            attributes: Vec::new(),
1✔
785
        });
1✔
786

787
        let first_request = doc.first_section(SectionType::Request);
1✔
788
        assert!(first_request.is_some());
1✔
789

790
        let first_error = doc.first_section(SectionType::Error);
1✔
791
        assert!(first_error.is_none());
1✔
792
    }
1✔
793

794
    #[test]
795
    fn test_gctf_document_get_address() {
1✔
796
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
797
        doc.sections.push(Section {
1✔
798
            section_type: SectionType::Address,
1✔
799
            content: SectionContent::Single("localhost:4770".to_string()),
1✔
800
            inline_options: InlineOptions::default(),
1✔
801
            raw_content: "".to_string(),
1✔
802
            start_line: 1,
1✔
803
            end_line: 1,
1✔
804
            attributes: Vec::new(),
1✔
805
        });
1✔
806

807
        assert_eq!(doc.get_address(None), Some("localhost:4770".to_string()));
1✔
808
        assert_eq!(
1✔
809
            doc.get_address(Some("env:5000")),
1✔
810
            Some("localhost:4770".to_string())
1✔
811
        );
812

813
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
814
        assert_eq!(
1✔
815
            doc2.get_address(Some("env:5000")),
1✔
816
            Some("env:5000".to_string())
1✔
817
        );
818
        assert_eq!(doc2.get_address(None), None);
1✔
819
    }
1✔
820

821
    #[test]
822
    fn test_gctf_document_get_endpoint() {
1✔
823
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
824
        doc.sections.push(Section {
1✔
825
            section_type: SectionType::Endpoint,
1✔
826
            content: SectionContent::Single("my.Service/Method".to_string()),
1✔
827
            inline_options: InlineOptions::default(),
1✔
828
            raw_content: "".to_string(),
1✔
829
            start_line: 1,
1✔
830
            end_line: 1,
1✔
831
            attributes: Vec::new(),
1✔
832
        });
1✔
833

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

836
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
837
        assert_eq!(doc2.get_endpoint(), None);
1✔
838
    }
1✔
839

840
    #[test]
841
    fn test_gctf_document_parse_endpoint() {
1✔
842
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
843
        doc.sections.push(Section {
1✔
844
            section_type: SectionType::Endpoint,
1✔
845
            content: SectionContent::Single("package.Service/Method".to_string()),
1✔
846
            inline_options: InlineOptions::default(),
1✔
847
            raw_content: "".to_string(),
1✔
848
            start_line: 1,
1✔
849
            end_line: 1,
1✔
850
            attributes: Vec::new(),
1✔
851
        });
1✔
852

853
        let (package, service, method) = doc.parse_endpoint().unwrap();
1✔
854
        assert_eq!(package, "package");
1✔
855
        assert_eq!(service, "Service");
1✔
856
        assert_eq!(method, "Method");
1✔
857
    }
1✔
858

859
    #[test]
860
    fn test_gctf_document_parse_endpoint_no_package() {
1✔
861
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
862
        doc.sections.push(Section {
1✔
863
            section_type: SectionType::Endpoint,
1✔
864
            content: SectionContent::Single("Service/Method".to_string()),
1✔
865
            inline_options: InlineOptions::default(),
1✔
866
            raw_content: "".to_string(),
1✔
867
            start_line: 1,
1✔
868
            end_line: 1,
1✔
869
            attributes: Vec::new(),
1✔
870
        });
1✔
871

872
        let (package, service, method) = doc.parse_endpoint().unwrap();
1✔
873
        assert_eq!(package, "");
1✔
874
        assert_eq!(service, "Service");
1✔
875
        assert_eq!(method, "Method");
1✔
876
    }
1✔
877

878
    #[test]
879
    fn test_gctf_document_parse_endpoint_invalid() {
1✔
880
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
881
        doc.sections.push(Section {
1✔
882
            section_type: SectionType::Endpoint,
1✔
883
            content: SectionContent::Single("invalid".to_string()),
1✔
884
            inline_options: InlineOptions::default(),
1✔
885
            raw_content: "".to_string(),
1✔
886
            start_line: 1,
1✔
887
            end_line: 1,
1✔
888
            attributes: Vec::new(),
1✔
889
        });
1✔
890

891
        assert!(doc.parse_endpoint().is_none());
1✔
892
    }
1✔
893

894
    #[test]
895
    fn test_gctf_document_get_requests() {
1✔
896
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
897
        doc.sections.push(Section {
1✔
898
            section_type: SectionType::Request,
1✔
899
            content: SectionContent::Json(json!({"key": "value1"})),
1✔
900
            inline_options: InlineOptions::default(),
1✔
901
            raw_content: "".to_string(),
1✔
902
            start_line: 1,
1✔
903
            end_line: 2,
1✔
904
            attributes: Vec::new(),
1✔
905
        });
1✔
906
        doc.sections.push(Section {
1✔
907
            section_type: SectionType::Request,
1✔
908
            content: SectionContent::Json(json!({"key": "value2"})),
1✔
909
            inline_options: InlineOptions::default(),
1✔
910
            raw_content: "".to_string(),
1✔
911
            start_line: 3,
1✔
912
            end_line: 4,
1✔
913
            attributes: Vec::new(),
1✔
914
        });
1✔
915

916
        let requests = doc.get_requests();
1✔
917
        assert_eq!(requests.len(), 2);
1✔
918
        assert_eq!(requests[0], json!({"key": "value1"}));
1✔
919
        assert_eq!(requests[1], json!({"key": "value2"}));
1✔
920
    }
1✔
921

922
    #[test]
923
    fn test_gctf_document_get_assertions() {
1✔
924
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
925
        doc.sections.push(Section {
1✔
926
            section_type: SectionType::Asserts,
1✔
927
            content: SectionContent::Assertions(vec![".id == 1".to_string()]),
1✔
928
            inline_options: InlineOptions::default(),
1✔
929
            raw_content: "".to_string(),
1✔
930
            start_line: 1,
1✔
931
            end_line: 2,
1✔
932
            attributes: Vec::new(),
1✔
933
        });
1✔
934
        doc.sections.push(Section {
1✔
935
            section_type: SectionType::Asserts,
1✔
936
            content: SectionContent::Assertions(vec![".name == \"test\"".to_string()]),
1✔
937
            inline_options: InlineOptions::default(),
1✔
938
            raw_content: "".to_string(),
1✔
939
            start_line: 3,
1✔
940
            end_line: 4,
1✔
941
            attributes: Vec::new(),
1✔
942
        });
1✔
943

944
        let assertions = doc.get_assertions();
1✔
945
        assert_eq!(assertions.len(), 2);
1✔
946
        assert_eq!(assertions[0], vec![".id == 1"]);
1✔
947
        assert_eq!(assertions[1], vec![".name == \"test\""]);
1✔
948
    }
1✔
949

950
    #[test]
951
    fn test_gctf_document_get_request_headers() {
1✔
952
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
953
        let mut headers = HashMap::new();
1✔
954
        headers.insert("Authorization".to_string(), "Bearer token".to_string());
1✔
955
        doc.sections.push(Section {
1✔
956
            section_type: SectionType::RequestHeaders,
1✔
957
            content: SectionContent::KeyValues(headers.clone()),
1✔
958
            inline_options: InlineOptions::default(),
1✔
959
            raw_content: "".to_string(),
1✔
960
            start_line: 1,
1✔
961
            end_line: 2,
1✔
962
            attributes: Vec::new(),
1✔
963
        });
1✔
964

965
        let result = doc.get_request_headers().unwrap();
1✔
966
        assert_eq!(
1✔
967
            result.get("Authorization"),
1✔
968
            Some(&"Bearer token".to_string())
1✔
969
        );
970
    }
1✔
971

972
    #[test]
973
    fn test_gctf_document_get_tls_config() {
1✔
974
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
975
        let mut config = HashMap::new();
1✔
976
        config.insert("ca_cert".to_string(), "/path/to/ca.pem".to_string());
1✔
977
        doc.sections.push(Section {
1✔
978
            section_type: SectionType::Tls,
1✔
979
            content: SectionContent::KeyValues(config.clone()),
1✔
980
            inline_options: InlineOptions::default(),
1✔
981
            raw_content: "".to_string(),
1✔
982
            start_line: 1,
1✔
983
            end_line: 2,
1✔
984
            attributes: Vec::new(),
1✔
985
        });
1✔
986

987
        let result = doc.get_tls_config().unwrap();
1✔
988
        assert_eq!(result.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1✔
989
    }
1✔
990

991
    #[test]
992
    fn test_gctf_document_get_options() {
1✔
993
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
994
        let mut options = HashMap::new();
1✔
995
        options.insert("dry_run".to_string(), "true".to_string());
1✔
996
        options.insert("timeout".to_string(), "10".to_string());
1✔
997
        doc.sections.push(Section {
1✔
998
            section_type: SectionType::Options,
1✔
999
            content: SectionContent::KeyValues(options.clone()),
1✔
1000
            inline_options: InlineOptions::default(),
1✔
1001
            raw_content: "".to_string(),
1✔
1002
            start_line: 1,
1✔
1003
            end_line: 2,
1✔
1004
            attributes: Vec::new(),
1✔
1005
        });
1✔
1006

1007
        let result = doc.get_options().unwrap();
1✔
1008
        assert_eq!(result.get("dry_run"), Some(&"true".to_string()));
1✔
1009
        assert_eq!(result.get("timeout"), Some(&"10".to_string()));
1✔
1010
    }
1✔
1011

1012
    #[test]
1013
    fn test_gctf_document_get_tls_config_with_defaults_env_only() {
1✔
1014
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1015
        let mut defaults = HashMap::new();
1✔
1016
        defaults.insert("server_name".to_string(), "example.com".to_string());
1✔
1017

1018
        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1✔
1019
        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1✔
1020
    }
1✔
1021

1022
    #[test]
1023
    fn test_gctf_document_get_tls_config_with_defaults_section_overrides() {
1✔
1024
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
1025
        let mut config = HashMap::new();
1✔
1026
        config.insert("insecure".to_string(), "true".to_string());
1✔
1027
        doc.sections.push(Section {
1✔
1028
            section_type: SectionType::Tls,
1✔
1029
            content: SectionContent::KeyValues(config),
1✔
1030
            inline_options: InlineOptions::default(),
1✔
1031
            raw_content: "".to_string(),
1✔
1032
            start_line: 1,
1✔
1033
            end_line: 2,
1✔
1034
            attributes: Vec::new(),
1✔
1035
        });
1✔
1036

1037
        let mut defaults = HashMap::new();
1✔
1038
        defaults.insert("insecure".to_string(), "false".to_string());
1✔
1039
        defaults.insert("server_name".to_string(), "example.com".to_string());
1✔
1040

1041
        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1✔
1042
        assert_eq!(result.get("insecure"), Some(&"true".to_string()));
1✔
1043
        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1✔
1044
    }
1✔
1045

1046
    #[test]
1047
    fn test_gctf_document_get_proto_config() {
1✔
1048
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
1049
        let mut config = HashMap::new();
1✔
1050
        config.insert("files".to_string(), "service.proto".to_string());
1✔
1051
        doc.sections.push(Section {
1✔
1052
            section_type: SectionType::Proto,
1✔
1053
            content: SectionContent::KeyValues(config.clone()),
1✔
1054
            inline_options: InlineOptions::default(),
1✔
1055
            raw_content: "".to_string(),
1✔
1056
            start_line: 1,
1✔
1057
            end_line: 2,
1✔
1058
            attributes: Vec::new(),
1✔
1059
        });
1✔
1060

1061
        let result = doc.get_proto_config().unwrap();
1✔
1062
        assert_eq!(result.get("files"), Some(&"service.proto".to_string()));
1✔
1063
    }
1✔
1064

1065
    #[test]
1066
    fn test_gctf_document_has_response_error_conflict() {
1✔
1067
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
1068
        assert!(!doc.has_response_error_conflict());
1✔
1069

1070
        doc.sections.push(Section {
1✔
1071
            section_type: SectionType::Response,
1✔
1072
            content: SectionContent::Json(json!({"result": "ok"})),
1✔
1073
            inline_options: InlineOptions::default(),
1✔
1074
            raw_content: "".to_string(),
1✔
1075
            start_line: 1,
1✔
1076
            end_line: 2,
1✔
1077
            attributes: Vec::new(),
1✔
1078
        });
1✔
1079
        assert!(!doc.has_response_error_conflict());
1✔
1080

1081
        doc.sections.push(Section {
1✔
1082
            section_type: SectionType::Error,
1✔
1083
            content: SectionContent::Json(json!({"code": 5})),
1✔
1084
            inline_options: InlineOptions::default(),
1✔
1085
            raw_content: "".to_string(),
1✔
1086
            start_line: 3,
1✔
1087
            end_line: 4,
1✔
1088
            attributes: Vec::new(),
1✔
1089
        });
1✔
1090
        assert!(doc.has_response_error_conflict());
1✔
1091
    }
1✔
1092

1093
    #[test]
1094
    fn test_inline_options_default() {
1✔
1095
        let options = InlineOptions::default();
1✔
1096
        assert!(!options.with_asserts);
1✔
1097
        assert!(!options.partial);
1✔
1098
        assert!(options.tolerance.is_none());
1✔
1099
        assert!(options.redact.is_empty());
1✔
1100
        assert!(!options.unordered_arrays);
1✔
1101
    }
1✔
1102

1103
    #[test]
1104
    fn test_section_format_header_with_inline_options() {
1✔
1105
        let section = Section {
1✔
1106
            section_type: SectionType::Response,
1✔
1107
            content: SectionContent::Json(serde_json::json!({"ok": true})),
1✔
1108
            inline_options: InlineOptions {
1✔
1109
                with_asserts: true,
1✔
1110
                partial: true,
1✔
1111
                tolerance: Some(0.1),
1✔
1112
                redact: vec!["token".to_string()],
1✔
1113
                unordered_arrays: true,
1✔
1114
            },
1✔
1115
            raw_content: "".to_string(),
1✔
1116
            start_line: 0,
1✔
1117
            end_line: 0,
1✔
1118
            attributes: Vec::new(),
1✔
1119
        };
1✔
1120

1121
        let header = section.format_header();
1✔
1122
        assert_eq!(
1✔
1123
            header,
1124
            "--- RESPONSE partial tolerance=0.1 redact=[\"token\"] unordered_arrays with_asserts ---"
1125
        );
1126
    }
1✔
1127

1128
    #[test]
1129
    fn test_section_content_debug() {
1✔
1130
        let content = SectionContent::Single("test".to_string());
1✔
1131
        let debug_str = format!("{:?}", content);
1✔
1132
        assert!(debug_str.contains("Single"));
1✔
1133
    }
1✔
1134

1135
    #[test]
1136
    fn test_section_header_keyword_from_source() {
1✔
1137
        let section = Section {
1✔
1138
            section_type: SectionType::Response,
1✔
1139
            content: SectionContent::Json(serde_json::json!({"ok": true})),
1✔
1140
            inline_options: InlineOptions::default(),
1✔
1141
            raw_content: "{\"ok\":true}".to_string(),
1✔
1142
            start_line: 0,
1✔
1143
            end_line: 2,
1✔
1144
            attributes: Vec::new(),
1✔
1145
        };
1✔
1146

1147
        let source = "--- RESPONSE with_asserts=true ---\n{\"ok\":true}\n";
1✔
1148
        assert_eq!(section.header_keyword_from_source(source), Some("RESPONSE"));
1✔
1149
    }
1✔
1150

1151
    #[test]
1152
    fn test_document_detects_deprecated_headers_alias() {
1✔
1153
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
1154
        doc.metadata.source = Some("--- HEADERS ---\nAuthorization: Bearer t\n".to_string());
1✔
1155
        doc.sections.push(Section {
1✔
1156
            section_type: SectionType::RequestHeaders,
1✔
1157
            content: SectionContent::KeyValues(HashMap::from([(
1✔
1158
                "Authorization".to_string(),
1✔
1159
                "Bearer t".to_string(),
1✔
1160
            )])),
1✔
1161
            inline_options: InlineOptions::default(),
1✔
1162
            raw_content: "Authorization: Bearer t".to_string(),
1✔
1163
            start_line: 0,
1✔
1164
            end_line: 2,
1✔
1165
            attributes: Vec::new(),
1✔
1166
        });
1✔
1167

1168
        assert!(doc.section_uses_deprecated_headers_alias(&doc.sections[0]));
1✔
1169
    }
1✔
1170

1171
    #[test]
1172
    fn test_gctf_document_debug() {
1✔
1173
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1174
        let debug_str = format!("{:?}", doc);
1✔
1175
        assert!(debug_str.contains("test.gctf"));
1✔
1176
    }
1✔
1177

1178
    // ─── Document chain (linked-list) tests ───
1179

1180
    #[test]
1181
    fn test_document_chain_single() {
1✔
1182
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1183
        assert!(doc.is_single_document());
1✔
1184
        assert_eq!(doc.document_count(), 1);
1✔
1185
    }
1✔
1186

1187
    #[test]
1188
    fn test_document_chain_two_docs() {
1✔
1189
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1190
        let doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1191
        doc1.next_document = Some(Box::new(doc2));
1✔
1192

1193
        assert!(!doc1.is_single_document());
1✔
1194
        assert_eq!(doc1.document_count(), 2);
1✔
1195
    }
1✔
1196

1197
    #[test]
1198
    fn test_document_chain_three_docs() {
1✔
1199
        let mut doc3 = GctfDocument::new("test.gctf".to_string());
1✔
1200
        doc3.file_path = "doc3".to_string();
1✔
1201

1202
        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1203
        doc2.file_path = "doc2".to_string();
1✔
1204
        doc2.next_document = Some(Box::new(doc3));
1✔
1205

1206
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1207
        doc1.file_path = "doc1".to_string();
1✔
1208
        doc1.next_document = Some(Box::new(doc2));
1✔
1209

1210
        assert_eq!(doc1.document_count(), 3);
1✔
1211

1212
        let docs: Vec<_> = doc1.iter_chain().collect();
1✔
1213
        assert_eq!(docs.len(), 3);
1✔
1214
        assert_eq!(docs[0].file_path, "doc1");
1✔
1215
        assert_eq!(docs[1].file_path, "doc2");
1✔
1216
        assert_eq!(docs[2].file_path, "doc3");
1✔
1217
    }
1✔
1218

1219
    #[test]
1220
    fn test_document_chain_get_document() {
1✔
1221
        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1✔
1222
        doc2.file_path = "doc2".to_string();
1✔
1223

1224
        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1✔
1225
        doc1.file_path = "doc1".to_string();
1✔
1226
        doc1.next_document = Some(Box::new(doc2));
1✔
1227

1228
        assert_eq!(doc1.get_document(0).unwrap().file_path, "doc1");
1✔
1229
        assert_eq!(doc1.get_document(1).unwrap().file_path, "doc2");
1✔
1230
        assert!(doc1.get_document(2).is_none());
1✔
1231
    }
1✔
1232

1233
    #[test]
1234
    fn test_document_chain_iter_on_last() {
1✔
1235
        let doc = GctfDocument::new("test.gctf".to_string());
1✔
1236
        let docs: Vec<_> = doc.iter_chain().collect();
1✔
1237
        assert_eq!(docs.len(), 1);
1✔
1238
        assert_eq!(docs[0].file_path, "test.gctf");
1✔
1239
    }
1✔
1240
}
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