Skip to main content

apif_ast/
ast.rs

1// AST (Abstract Syntax Tree) for .gctf files
2// Represents the parsed structure of a .gctf test file
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Complete .gctf document
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub 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
24pub struct DocumentChainIter<'a> {
25    current: Option<&'a GctfDocument>,
26}
27
28impl<'a> Iterator for DocumentChainIter<'a> {
29    type Item = &'a GctfDocument;
30
31    fn next(&mut self) -> Option<Self::Item> {
32        let current = self.current?;
33        self.current = current.next_document.as_deref();
34        Some(current)
35    }
36}
37
38/// Document metadata
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub 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    /// Variable type annotations from EXTRACT sections: `name:Type = .path`
51    /// Maps variable name → type name (e.g., "number", "string").
52    /// Used for {{var}} type inference in assertions.
53    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
54    pub variable_types: HashMap<String, String>,
55}
56
57impl Default for DocumentMetadata {
58    fn default() -> Self {
59        Self {
60            source: None,
61            mtime: None,
62            parsed_at: if cfg!(miri) {
63                0
64            } else {
65                std::time::SystemTime::now()
66                    .duration_since(std::time::UNIX_EPOCH)
67                    .map(|d| d.as_secs() as i64)
68                    .unwrap_or(0)
69            },
70            variable_types: HashMap::new(),
71        }
72    }
73}
74
75/// File-level metadata (META section)
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
77#[serde(default)]
78pub struct FileMeta {
79    /// Test name
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub name: Option<String>,
82    /// Test summary (one-liner)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub summary: Option<String>,
85    /// Test tags
86    #[serde(skip_serializing_if = "Vec::is_empty")]
87    pub tags: Vec<String>,
88    /// Test owner (team/person)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub owner: Option<String>,
91    /// Related links (docs, jira, etc)
92    #[serde(skip_serializing_if = "Vec::is_empty")]
93    pub links: Vec<String>,
94}
95
96impl FileMeta {
97    /// Check if meta has any content
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        self.name.is_none()
101            && self.summary.is_none()
102            && self.tags.is_empty()
103            && self.owner.is_none()
104            && self.links.is_empty()
105    }
106}
107
108/// GCTF attribute (#[name(value)] syntax)
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct GctfAttribute {
111    pub name: String,
112    pub value: String,
113}
114
115impl GctfAttribute {
116    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
117        Self {
118            name: name.into(),
119            value: value.into(),
120        }
121    }
122
123    pub fn flag(name: impl Into<String>) -> Self {
124        Self {
125            name: name.into(),
126            value: "true".into(),
127        }
128    }
129
130    pub fn parse_u64(&self) -> Option<u64> {
131        self.value.trim().parse::<u64>().ok()
132    }
133
134    pub fn parse_u32(&self) -> Option<u32> {
135        self.value.trim().parse::<u32>().ok()
136    }
137
138    pub fn parse_f64(&self) -> Option<f64> {
139        self.value.trim().parse::<f64>().ok()
140    }
141
142    pub fn parse_bool(&self) -> Option<bool> {
143        match self.value.trim().to_lowercase().as_str() {
144            "true" | "1" | "yes" | "on" => Some(true),
145            "false" | "0" | "no" | "off" | "" => Some(false),
146            _ => None,
147        }
148    }
149
150    pub fn as_str(&self) -> &str {
151        &self.value
152    }
153
154    pub fn format_directive(&self) -> String {
155        if self.value == "true" {
156            format!("#[{}]", self.name)
157        } else {
158            format!("#[{}({})]", self.name, self.value)
159        }
160    }
161}
162
163impl Section {
164    pub fn get_attribute(&self, name: &str) -> Option<&GctfAttribute> {
165        self.attributes.iter().find(|a| a.name == name)
166    }
167
168    pub fn get_timeout(&self) -> Option<u64> {
169        self.get_attribute("timeout")
170            .and_then(|a| a.parse_u64())
171            .filter(|&v| v > 0)
172    }
173
174    pub fn get_retry(&self) -> Option<u32> {
175        self.get_attribute("retry").and_then(|a| a.parse_u32())
176    }
177
178    pub fn get_skip(&self) -> bool {
179        self.get_attribute("skip")
180            .and_then(|a| a.parse_bool())
181            .unwrap_or(false)
182    }
183
184    #[must_use]
185    pub fn has_tag(&self, tag: &str) -> bool {
186        self.get_attribute("tag")
187            .is_some_and(|a| a.value.split(',').any(|t| t.trim() == tag))
188    }
189
190    pub fn get_compression(&self) -> Option<String> {
191        self.get_attribute("compression")
192            .map(|a| a.value.trim().to_lowercase())
193            .filter(|v| v == "gzip" || v == "none")
194    }
195
196    pub fn get_repeat(&self) -> Option<u32> {
197        self.get_attribute("repeat")
198            .and_then(|a| a.parse_u32())
199            .filter(|&v| v >= 1)
200    }
201}
202
203/// A section in the .gctf file
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct Section {
206    /// Section type
207    pub section_type: SectionType,
208
209    /// Content of the section (raw text, typically JSON)
210    pub content: SectionContent,
211
212    /// Inline options (for sections that support them)
213    pub inline_options: InlineOptions,
214
215    /// Raw text content of the section (preserved for formatting)
216    pub raw_content: String,
217
218    /// Line number where section starts
219    pub start_line: usize,
220
221    /// Line number where section ends
222    pub end_line: usize,
223
224    /// Attributes declared on this section
225    #[serde(default, skip_serializing_if = "Vec::is_empty")]
226    pub attributes: Vec<GctfAttribute>,
227}
228
229impl Default for Section {
230    fn default() -> Self {
231        Self {
232            section_type: SectionType::Address,
233            content: SectionContent::Empty,
234            inline_options: InlineOptions::default(),
235            raw_content: String::new(),
236            start_line: 0,
237            end_line: 0,
238            attributes: Vec::new(),
239        }
240    }
241}
242
243/// Section content
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub enum SectionContent {
246    /// Single value (ADDRESS, ENDPOINT, etc.)
247    Single(String),
248
249    /// JSON object (REQUEST, RESPONSE, ERROR)
250    Json(serde_json::Value),
251
252    /// Newline-delimited JSON values within a single section block
253    JsonLines(Vec<serde_json::Value>),
254
255    /// Key-value pairs (REQUEST_HEADERS, TLS, OPTIONS, PROTO)
256    KeyValues(HashMap<String, String>),
257
258    /// Extract variables from response (EXTRACT)
259    Extract(HashMap<String, String>),
260
261    /// Assertion expressions (ASSERTS)
262    Assertions(Vec<String>),
263
264    /// File-level metadata (META)
265    Meta(FileMeta),
266
267    /// Empty section
268    Empty,
269}
270
271/// Section types in .gctf files
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub enum SectionType {
274    /// Server address
275    Address,
276
277    /// gRPC endpoint (service/method)
278    Endpoint,
279
280    /// Request payload (can have multiple)
281    Request,
282
283    /// Expected response (can have multiple)
284    Response,
285
286    /// Expected error
287    Error,
288
289    /// Request-specific headers
290    RequestHeaders,
291
292    /// Assertion expressions (can have multiple)
293    Asserts,
294
295    /// Protocol buffer configuration
296    Proto,
297
298    /// TLS/mTLS configuration
299    Tls,
300
301    /// Test execution options
302    Options,
303
304    /// Extract variables from response
305    Extract,
306
307    /// File-level metadata (suite, tags)
308    Meta,
309
310    /// File-level benchmark profile/options
311    Bench,
312}
313
314impl SectionType {
315    /// Returns `true` if this section marks the end of a logical request-response cycle.
316    #[must_use]
317    pub fn is_terminal(&self) -> bool {
318        matches!(
319            self,
320            SectionType::Response | SectionType::Error | SectionType::Asserts
321        )
322    }
323
324    /// Get section name as string
325    pub fn as_str(&self) -> &'static str {
326        match self {
327            SectionType::Address => "ADDRESS",
328            SectionType::Endpoint => "ENDPOINT",
329            SectionType::Request => "REQUEST",
330            SectionType::Response => "RESPONSE",
331            SectionType::Error => "ERROR",
332            SectionType::RequestHeaders => "REQUEST_HEADERS",
333            SectionType::Asserts => "ASSERTS",
334            SectionType::Proto => "PROTO",
335            SectionType::Tls => "TLS",
336            SectionType::Options => "OPTIONS",
337            SectionType::Extract => "EXTRACT",
338            SectionType::Meta => "META",
339            SectionType::Bench => "BENCH",
340        }
341    }
342
343    /// Parse section name string to SectionType
344    pub fn from_keyword(s: &str) -> Option<SectionType> {
345        match s.trim() {
346            "ADDRESS" => Some(SectionType::Address),
347            "ENDPOINT" => Some(SectionType::Endpoint),
348            "REQUEST" => Some(SectionType::Request),
349            "RESPONSE" => Some(SectionType::Response),
350            "ERROR" => Some(SectionType::Error),
351            "REQUEST_HEADERS" | "HEADERS" => Some(SectionType::RequestHeaders),
352            "ASSERTS" => Some(SectionType::Asserts),
353            "PROTO" => Some(SectionType::Proto),
354            "TLS" => Some(SectionType::Tls),
355            "OPTIONS" => Some(SectionType::Options),
356            "EXTRACT" => Some(SectionType::Extract),
357            "META" => Some(SectionType::Meta),
358            "BENCH" => Some(SectionType::Bench),
359            _ => None,
360        }
361    }
362
363    /// Check if section can appear multiple times
364    #[must_use]
365    pub fn is_multiple_allowed(&self) -> bool {
366        matches!(
367            self,
368            SectionType::Request
369                | SectionType::Response
370                | SectionType::Asserts
371                | SectionType::Extract
372        )
373    }
374
375    /// Check if section is file-level (not inside documents)
376    #[must_use]
377    pub fn is_file_level(&self) -> bool {
378        matches!(self, SectionType::Meta | SectionType::Bench)
379    }
380
381    pub fn supports_inline_options(&self) -> bool {
382        matches!(self, SectionType::Response | SectionType::Error)
383    }
384
385    pub fn preamble_rank(&self) -> Option<usize> {
386        match self {
387            SectionType::Meta => Some(0),
388            SectionType::Bench => Some(1),
389            SectionType::Address => Some(2),
390            SectionType::Endpoint => Some(3),
391            SectionType::Tls => Some(4),
392            SectionType::Proto => Some(5),
393            SectionType::Options => Some(6),
394            _ => None,
395        }
396    }
397}
398
399/// Inline options for sections
400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
401pub struct InlineOptions {
402    /// Run ASSERTS on same response (unary RPC)
403    pub with_asserts: bool,
404
405    /// Subset comparison (expected is subset of actual)
406    pub partial: bool,
407
408    /// Numeric tolerance for floating-point comparisons
409    pub tolerance: Option<f64>,
410
411    /// Remove sensitive fields before comparison
412    pub redact: Vec<String>,
413
414    /// Sort arrays for order-independent comparison
415    pub unordered_arrays: bool,
416}
417
418impl InlineOptions {
419    pub fn to_header_tokens(&self) -> Vec<String> {
420        let mut parts = Vec::new();
421
422        if self.partial {
423            parts.push("partial".to_string());
424        }
425
426        if let Some(tolerance) = self.tolerance {
427            parts.push(format!("tolerance={}", tolerance));
428        }
429
430        if !self.redact.is_empty() {
431            let mut sorted_redact = self.redact.clone();
432            sorted_redact.sort();
433            let quoted = sorted_redact
434                .iter()
435                .map(|field| format!("\"{}\"", field))
436                .collect::<Vec<_>>()
437                .join(",");
438            parts.push(format!("redact=[{}]", quoted));
439        }
440
441        if self.unordered_arrays {
442            parts.push("unordered_arrays".to_string());
443        }
444
445        if self.with_asserts {
446            parts.push("with_asserts".to_string());
447        }
448
449        parts
450    }
451
452    #[must_use]
453    pub fn is_empty(&self) -> bool {
454        !self.with_asserts
455            && !self.partial
456            && self.tolerance.is_none()
457            && self.redact.is_empty()
458            && !self.unordered_arrays
459    }
460}
461
462impl Section {
463    pub fn format_header(&self) -> String {
464        let section = self.section_type.as_str();
465        if self.section_type.supports_inline_options() {
466            let parts = self.inline_options.to_header_tokens();
467            if parts.is_empty() {
468                format!("--- {} ---", section)
469            } else {
470                format!("--- {} {} ---", section, parts.join(" "))
471            }
472        } else {
473            format!("--- {} ---", section)
474        }
475    }
476
477    pub fn header_keyword_from_source<'a>(&self, source: &'a str) -> Option<&'a str> {
478        let header_line = source.lines().nth(self.start_line)?.trim();
479        let inner = header_line.strip_prefix("---")?.strip_suffix("---")?.trim();
480        inner.split_whitespace().next()
481    }
482}
483
484/// GCTF file header with inline options
485/// Format: --- SECTION_NAME key=value ... ---
486#[derive(Debug, Clone, PartialEq)]
487pub struct SectionHeader {
488    /// Section type
489    pub section_type: SectionType,
490
491    /// Inline options (key=value pairs)
492    pub options: HashMap<String, String>,
493}
494
495impl GctfDocument {
496    /// Create a new empty document
497    pub fn new(file_path: String) -> Self {
498        Self {
499            file_path,
500            sections: Vec::new(),
501            metadata: DocumentMetadata::default(),
502            next_document: None,
503        }
504    }
505
506    /// Get document by index (0-based) from the chain
507    pub fn get_document(&self, index: usize) -> Option<&GctfDocument> {
508        self.iter_chain().nth(index)
509    }
510
511    pub fn iter_chain(&self) -> DocumentChainIter<'_> {
512        DocumentChainIter {
513            current: Some(self),
514        }
515    }
516
517    pub fn document_count(&self) -> usize {
518        let mut count = 1;
519        let mut current = &self.next_document;
520        while let Some(doc) = current {
521            count += 1;
522            current = &doc.next_document;
523        }
524        count
525    }
526
527    #[must_use]
528    pub fn is_single_document(&self) -> bool {
529        self.next_document.is_none()
530    }
531
532    /// Get all sections of a specific type
533    pub fn sections_by_type(&self, section_type: SectionType) -> Vec<&Section> {
534        self.sections
535            .iter()
536            .filter(|s| s.section_type == section_type)
537            .collect()
538    }
539
540    /// Get first section of a specific type
541    pub fn first_section(&self, section_type: SectionType) -> Option<&Section> {
542        self.sections
543            .iter()
544            .find(|s| s.section_type == section_type)
545    }
546
547    /// Get address (from ADDRESS section or environment variable)
548    pub fn get_address(&self, env_address: Option<&str>) -> Option<String> {
549        if let Some(section) = self.first_section(SectionType::Address)
550            && let SectionContent::Single(addr) = &section.content
551        {
552            return Some(addr.clone());
553        }
554        env_address.map(|s| s.to_string())
555    }
556
557    /// Get endpoint
558    pub fn get_endpoint(&self) -> Option<String> {
559        if let Some(section) = self.first_section(SectionType::Endpoint)
560            && let SectionContent::Single(endpoint) = &section.content
561        {
562            return Some(endpoint.clone());
563        }
564        None
565    }
566
567    /// Parse endpoint into package, service, method
568    pub fn parse_endpoint(&self) -> Option<(String, String, String)> {
569        let endpoint = self.get_endpoint()?;
570        let parts: Vec<&str> = endpoint.split('/').collect();
571        if parts.len() == 2 {
572            let full_service = parts[0];
573            let service_parts: Vec<&str> = full_service.split('.').collect();
574            if service_parts.len() >= 2 {
575                let package = service_parts[..service_parts.len() - 1].join(".");
576                let service = service_parts[service_parts.len() - 1].to_string();
577                let method = parts[1].to_string();
578                return Some((package, service, method));
579            } else if service_parts.len() == 1 {
580                let package = String::new();
581                let service = service_parts[0].to_string();
582                let method = parts[1].to_string();
583                return Some((package, service, method));
584            }
585        }
586        None
587    }
588
589    /// Get all request payloads
590    pub fn get_requests(&self) -> Vec<serde_json::Value> {
591        self.sections_by_type(SectionType::Request)
592            .into_iter()
593            .filter_map(|s| {
594                if let SectionContent::Json(json) = &s.content {
595                    Some(json.clone())
596                } else {
597                    None
598                }
599            })
600            .collect()
601    }
602
603    /// Get all assertion sections
604    pub fn get_assertions(&self) -> Vec<Vec<String>> {
605        self.sections_by_type(SectionType::Asserts)
606            .into_iter()
607            .filter_map(|s| {
608                if let SectionContent::Assertions(asserts) = &s.content {
609                    Some(asserts.clone())
610                } else {
611                    None
612                }
613            })
614            .collect()
615    }
616
617    /// Get request headers
618    pub fn get_request_headers(&self) -> Option<HashMap<String, String>> {
619        if let Some(section) = self.first_section(SectionType::RequestHeaders)
620            && let SectionContent::KeyValues(headers) = &section.content
621        {
622            return Some(headers.clone());
623        }
624        None
625    }
626
627    /// Get TLS configuration
628    pub fn get_tls_config(&self) -> Option<HashMap<String, String>> {
629        if let Some(section) = self.first_section(SectionType::Tls)
630            && let SectionContent::KeyValues(config) = &section.content
631        {
632            return Some(config.clone());
633        }
634        None
635    }
636
637    /// Get OPTIONS configuration
638    pub fn get_options(&self) -> Option<HashMap<String, String>> {
639        if let Some(section) = self.first_section(SectionType::Options)
640            && let SectionContent::KeyValues(config) = &section.content
641        {
642            return Some(config.clone());
643        }
644        None
645    }
646
647    /// Get TLS configuration merged with defaults (section values override defaults)
648    pub fn get_tls_config_with_defaults(
649        &self,
650        defaults: &HashMap<String, String>,
651    ) -> Option<HashMap<String, String>> {
652        let mut merged = defaults.clone();
653
654        if let Some(section) = self.first_section(SectionType::Tls)
655            && let SectionContent::KeyValues(config) = &section.content
656        {
657            for (key, value) in config {
658                merged.insert(key.clone(), value.clone());
659            }
660        }
661
662        if merged.is_empty() {
663            None
664        } else {
665            Some(merged)
666        }
667    }
668
669    /// Get PROTO configuration
670    pub fn get_proto_config(&self) -> Option<HashMap<String, String>> {
671        if let Some(section) = self.first_section(SectionType::Proto)
672            && let SectionContent::KeyValues(config) = &section.content
673        {
674            return Some(config.clone());
675        }
676        None
677    }
678
679    /// Check for RESPONSE and ERROR conflict
680    #[must_use]
681    pub fn has_response_error_conflict(&self) -> bool {
682        self.first_section(SectionType::Response).is_some()
683            && self.first_section(SectionType::Error).is_some()
684    }
685
686    pub fn section_uses_deprecated_headers_alias(&self, section: &Section) -> bool {
687        if section.section_type != SectionType::RequestHeaders {
688            return false;
689        }
690
691        self.metadata
692            .source
693            .as_deref()
694            .and_then(|source| section.header_keyword_from_source(source))
695            .is_some_and(|keyword| keyword.eq_ignore_ascii_case("HEADERS"))
696    }
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use serde_json::json;
703
704    #[test]
705    fn test_section_type_from_str() {
706        assert_eq!(
707            SectionType::from_keyword("ADDRESS"),
708            Some(SectionType::Address)
709        );
710        assert_eq!(
711            SectionType::from_keyword("ENDPOINT"),
712            Some(SectionType::Endpoint)
713        );
714        assert_eq!(SectionType::from_keyword("INVALID"), None);
715    }
716
717    #[test]
718    fn test_section_type_multiple_allowed() {
719        assert!(SectionType::Request.is_multiple_allowed());
720        assert!(SectionType::Response.is_multiple_allowed());
721        assert!(SectionType::Asserts.is_multiple_allowed());
722        assert!(!SectionType::Address.is_multiple_allowed());
723        assert!(!SectionType::Endpoint.is_multiple_allowed());
724    }
725
726    #[test]
727    fn test_section_type_supports_inline_options() {
728        assert!(SectionType::Response.supports_inline_options());
729        assert!(SectionType::Error.supports_inline_options());
730        assert!(!SectionType::Request.supports_inline_options());
731        assert!(!SectionType::Address.supports_inline_options());
732    }
733
734    #[test]
735    fn test_section_type_as_str() {
736        assert_eq!(SectionType::Address.as_str(), "ADDRESS");
737        assert_eq!(SectionType::Endpoint.as_str(), "ENDPOINT");
738        assert_eq!(SectionType::Request.as_str(), "REQUEST");
739        assert_eq!(SectionType::Response.as_str(), "RESPONSE");
740        assert_eq!(SectionType::Error.as_str(), "ERROR");
741        assert_eq!(SectionType::RequestHeaders.as_str(), "REQUEST_HEADERS");
742        assert_eq!(SectionType::Asserts.as_str(), "ASSERTS");
743        assert_eq!(SectionType::Proto.as_str(), "PROTO");
744        assert_eq!(SectionType::Tls.as_str(), "TLS");
745        assert_eq!(SectionType::Options.as_str(), "OPTIONS");
746        assert_eq!(SectionType::Extract.as_str(), "EXTRACT");
747        assert_eq!(SectionType::Meta.as_str(), "META");
748        assert_eq!(SectionType::Bench.as_str(), "BENCH");
749    }
750
751    #[test]
752    fn test_section_type_from_keyword_aliases() {
753        assert_eq!(
754            SectionType::from_keyword("HEADERS"),
755            Some(SectionType::RequestHeaders)
756        );
757        assert_eq!(
758            SectionType::from_keyword("REQUEST_HEADERS"),
759            Some(SectionType::RequestHeaders)
760        );
761        assert_eq!(SectionType::from_keyword("BENCH"), Some(SectionType::Bench));
762    }
763
764    #[test]
765    fn test_section_type_from_keyword_case_insensitive() {
766        // Should be case sensitive based on implementation
767        assert_eq!(SectionType::from_keyword("address"), None);
768        assert_eq!(
769            SectionType::from_keyword("  ADDRESS  "),
770            Some(SectionType::Address)
771        );
772    }
773
774    #[test]
775    fn test_section_type_is_terminal() {
776        assert!(SectionType::Response.is_terminal());
777        assert!(SectionType::Error.is_terminal());
778        assert!(SectionType::Asserts.is_terminal());
779        assert!(!SectionType::Request.is_terminal());
780        assert!(!SectionType::Endpoint.is_terminal());
781        assert!(!SectionType::Extract.is_terminal());
782        assert!(!SectionType::Address.is_terminal());
783    }
784
785    #[test]
786    fn test_gctf_document_new() {
787        let doc = GctfDocument::new("test.gctf".to_string());
788        assert_eq!(doc.file_path, "test.gctf");
789        assert!(doc.sections.is_empty());
790        assert!(doc.metadata.source.is_none());
791        assert!(doc.metadata.mtime.is_none());
792    }
793
794    #[test]
795    fn test_gctf_document_sections_by_type() {
796        let mut doc = GctfDocument::new("test.gctf".to_string());
797        doc.sections.push(Section {
798            section_type: SectionType::Request,
799            content: SectionContent::Json(json!({"key": "value1"})),
800            inline_options: InlineOptions::default(),
801            raw_content: "".to_string(),
802            start_line: 1,
803            end_line: 2,
804            attributes: Vec::new(),
805        });
806        doc.sections.push(Section {
807            section_type: SectionType::Request,
808            content: SectionContent::Json(json!({"key": "value2"})),
809            inline_options: InlineOptions::default(),
810            raw_content: "".to_string(),
811            start_line: 3,
812            end_line: 4,
813            attributes: Vec::new(),
814        });
815        doc.sections.push(Section {
816            section_type: SectionType::Response,
817            content: SectionContent::Json(json!({"result": "ok"})),
818            inline_options: InlineOptions::default(),
819            raw_content: "".to_string(),
820            start_line: 5,
821            end_line: 6,
822            attributes: Vec::new(),
823        });
824
825        let requests = doc.sections_by_type(SectionType::Request);
826        assert_eq!(requests.len(), 2);
827
828        let responses = doc.sections_by_type(SectionType::Response);
829        assert_eq!(responses.len(), 1);
830
831        let errors = doc.sections_by_type(SectionType::Error);
832        assert_eq!(errors.len(), 0);
833    }
834
835    #[test]
836    fn test_gctf_document_first_section() {
837        let mut doc = GctfDocument::new("test.gctf".to_string());
838        doc.sections.push(Section {
839            section_type: SectionType::Request,
840            content: SectionContent::Json(json!({"key": "value"})),
841            inline_options: InlineOptions::default(),
842            raw_content: "".to_string(),
843            start_line: 1,
844            end_line: 2,
845            attributes: Vec::new(),
846        });
847
848        let first_request = doc.first_section(SectionType::Request);
849        assert!(first_request.is_some());
850
851        let first_error = doc.first_section(SectionType::Error);
852        assert!(first_error.is_none());
853    }
854
855    #[test]
856    fn test_gctf_document_get_address() {
857        let mut doc = GctfDocument::new("test.gctf".to_string());
858        doc.sections.push(Section {
859            section_type: SectionType::Address,
860            content: SectionContent::Single("localhost:4770".to_string()),
861            inline_options: InlineOptions::default(),
862            raw_content: "".to_string(),
863            start_line: 1,
864            end_line: 1,
865            attributes: Vec::new(),
866        });
867
868        assert_eq!(doc.get_address(None), Some("localhost:4770".to_string()));
869        assert_eq!(
870            doc.get_address(Some("env:5000")),
871            Some("localhost:4770".to_string())
872        );
873
874        let doc2 = GctfDocument::new("test.gctf".to_string());
875        assert_eq!(
876            doc2.get_address(Some("env:5000")),
877            Some("env:5000".to_string())
878        );
879        assert_eq!(doc2.get_address(None), None);
880    }
881
882    #[test]
883    fn test_gctf_document_get_endpoint() {
884        let mut doc = GctfDocument::new("test.gctf".to_string());
885        doc.sections.push(Section {
886            section_type: SectionType::Endpoint,
887            content: SectionContent::Single("my.Service/Method".to_string()),
888            inline_options: InlineOptions::default(),
889            raw_content: "".to_string(),
890            start_line: 1,
891            end_line: 1,
892            attributes: Vec::new(),
893        });
894
895        assert_eq!(doc.get_endpoint(), Some("my.Service/Method".to_string()));
896
897        let doc2 = GctfDocument::new("test.gctf".to_string());
898        assert_eq!(doc2.get_endpoint(), None);
899    }
900
901    #[test]
902    fn test_gctf_document_parse_endpoint() {
903        let mut doc = GctfDocument::new("test.gctf".to_string());
904        doc.sections.push(Section {
905            section_type: SectionType::Endpoint,
906            content: SectionContent::Single("package.Service/Method".to_string()),
907            inline_options: InlineOptions::default(),
908            raw_content: "".to_string(),
909            start_line: 1,
910            end_line: 1,
911            attributes: Vec::new(),
912        });
913
914        let (package, service, method) = doc.parse_endpoint().unwrap();
915        assert_eq!(package, "package");
916        assert_eq!(service, "Service");
917        assert_eq!(method, "Method");
918    }
919
920    #[test]
921    fn test_gctf_document_parse_endpoint_no_package() {
922        let mut doc = GctfDocument::new("test.gctf".to_string());
923        doc.sections.push(Section {
924            section_type: SectionType::Endpoint,
925            content: SectionContent::Single("Service/Method".to_string()),
926            inline_options: InlineOptions::default(),
927            raw_content: "".to_string(),
928            start_line: 1,
929            end_line: 1,
930            attributes: Vec::new(),
931        });
932
933        let (package, service, method) = doc.parse_endpoint().unwrap();
934        assert_eq!(package, "");
935        assert_eq!(service, "Service");
936        assert_eq!(method, "Method");
937    }
938
939    #[test]
940    fn test_gctf_document_parse_endpoint_invalid() {
941        let mut doc = GctfDocument::new("test.gctf".to_string());
942        doc.sections.push(Section {
943            section_type: SectionType::Endpoint,
944            content: SectionContent::Single("invalid".to_string()),
945            inline_options: InlineOptions::default(),
946            raw_content: "".to_string(),
947            start_line: 1,
948            end_line: 1,
949            attributes: Vec::new(),
950        });
951
952        assert!(doc.parse_endpoint().is_none());
953    }
954
955    #[test]
956    fn test_gctf_document_get_requests() {
957        let mut doc = GctfDocument::new("test.gctf".to_string());
958        doc.sections.push(Section {
959            section_type: SectionType::Request,
960            content: SectionContent::Json(json!({"key": "value1"})),
961            inline_options: InlineOptions::default(),
962            raw_content: "".to_string(),
963            start_line: 1,
964            end_line: 2,
965            attributes: Vec::new(),
966        });
967        doc.sections.push(Section {
968            section_type: SectionType::Request,
969            content: SectionContent::Json(json!({"key": "value2"})),
970            inline_options: InlineOptions::default(),
971            raw_content: "".to_string(),
972            start_line: 3,
973            end_line: 4,
974            attributes: Vec::new(),
975        });
976
977        let requests = doc.get_requests();
978        assert_eq!(requests.len(), 2);
979        assert_eq!(requests[0], json!({"key": "value1"}));
980        assert_eq!(requests[1], json!({"key": "value2"}));
981    }
982
983    #[test]
984    fn test_gctf_document_get_assertions() {
985        let mut doc = GctfDocument::new("test.gctf".to_string());
986        doc.sections.push(Section {
987            section_type: SectionType::Asserts,
988            content: SectionContent::Assertions(vec![".id == 1".to_string()]),
989            inline_options: InlineOptions::default(),
990            raw_content: "".to_string(),
991            start_line: 1,
992            end_line: 2,
993            attributes: Vec::new(),
994        });
995        doc.sections.push(Section {
996            section_type: SectionType::Asserts,
997            content: SectionContent::Assertions(vec![".name == \"test\"".to_string()]),
998            inline_options: InlineOptions::default(),
999            raw_content: "".to_string(),
1000            start_line: 3,
1001            end_line: 4,
1002            attributes: Vec::new(),
1003        });
1004
1005        let assertions = doc.get_assertions();
1006        assert_eq!(assertions.len(), 2);
1007        assert_eq!(assertions[0], vec![".id == 1"]);
1008        assert_eq!(assertions[1], vec![".name == \"test\""]);
1009    }
1010
1011    #[test]
1012    fn test_gctf_document_get_request_headers() {
1013        let mut doc = GctfDocument::new("test.gctf".to_string());
1014        let mut headers = HashMap::new();
1015        headers.insert("Authorization".to_string(), "Bearer token".to_string());
1016        doc.sections.push(Section {
1017            section_type: SectionType::RequestHeaders,
1018            content: SectionContent::KeyValues(headers.clone()),
1019            inline_options: InlineOptions::default(),
1020            raw_content: "".to_string(),
1021            start_line: 1,
1022            end_line: 2,
1023            attributes: Vec::new(),
1024        });
1025
1026        let result = doc.get_request_headers().unwrap();
1027        assert_eq!(
1028            result.get("Authorization"),
1029            Some(&"Bearer token".to_string())
1030        );
1031    }
1032
1033    #[test]
1034    fn test_gctf_document_get_tls_config() {
1035        let mut doc = GctfDocument::new("test.gctf".to_string());
1036        let mut config = HashMap::new();
1037        config.insert("ca_cert".to_string(), "/path/to/ca.pem".to_string());
1038        doc.sections.push(Section {
1039            section_type: SectionType::Tls,
1040            content: SectionContent::KeyValues(config.clone()),
1041            inline_options: InlineOptions::default(),
1042            raw_content: "".to_string(),
1043            start_line: 1,
1044            end_line: 2,
1045            attributes: Vec::new(),
1046        });
1047
1048        let result = doc.get_tls_config().unwrap();
1049        assert_eq!(result.get("ca_cert"), Some(&"/path/to/ca.pem".to_string()));
1050    }
1051
1052    #[test]
1053    fn test_gctf_document_get_options() {
1054        let mut doc = GctfDocument::new("test.gctf".to_string());
1055        let mut options = HashMap::new();
1056        options.insert("dry_run".to_string(), "true".to_string());
1057        options.insert("timeout".to_string(), "10".to_string());
1058        doc.sections.push(Section {
1059            section_type: SectionType::Options,
1060            content: SectionContent::KeyValues(options.clone()),
1061            inline_options: InlineOptions::default(),
1062            raw_content: "".to_string(),
1063            start_line: 1,
1064            end_line: 2,
1065            attributes: Vec::new(),
1066        });
1067
1068        let result = doc.get_options().unwrap();
1069        assert_eq!(result.get("dry_run"), Some(&"true".to_string()));
1070        assert_eq!(result.get("timeout"), Some(&"10".to_string()));
1071    }
1072
1073    #[test]
1074    fn test_gctf_document_get_tls_config_with_defaults_env_only() {
1075        let doc = GctfDocument::new("test.gctf".to_string());
1076        let mut defaults = HashMap::new();
1077        defaults.insert("server_name".to_string(), "example.com".to_string());
1078
1079        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1080        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1081    }
1082
1083    #[test]
1084    fn test_gctf_document_get_tls_config_with_defaults_section_overrides() {
1085        let mut doc = GctfDocument::new("test.gctf".to_string());
1086        let mut config = HashMap::new();
1087        config.insert("insecure".to_string(), "true".to_string());
1088        doc.sections.push(Section {
1089            section_type: SectionType::Tls,
1090            content: SectionContent::KeyValues(config),
1091            inline_options: InlineOptions::default(),
1092            raw_content: "".to_string(),
1093            start_line: 1,
1094            end_line: 2,
1095            attributes: Vec::new(),
1096        });
1097
1098        let mut defaults = HashMap::new();
1099        defaults.insert("insecure".to_string(), "false".to_string());
1100        defaults.insert("server_name".to_string(), "example.com".to_string());
1101
1102        let result = doc.get_tls_config_with_defaults(&defaults).unwrap();
1103        assert_eq!(result.get("insecure"), Some(&"true".to_string()));
1104        assert_eq!(result.get("server_name"), Some(&"example.com".to_string()));
1105    }
1106
1107    #[test]
1108    fn test_gctf_document_get_proto_config() {
1109        let mut doc = GctfDocument::new("test.gctf".to_string());
1110        let mut config = HashMap::new();
1111        config.insert("files".to_string(), "service.proto".to_string());
1112        doc.sections.push(Section {
1113            section_type: SectionType::Proto,
1114            content: SectionContent::KeyValues(config.clone()),
1115            inline_options: InlineOptions::default(),
1116            raw_content: "".to_string(),
1117            start_line: 1,
1118            end_line: 2,
1119            attributes: Vec::new(),
1120        });
1121
1122        let result = doc.get_proto_config().unwrap();
1123        assert_eq!(result.get("files"), Some(&"service.proto".to_string()));
1124    }
1125
1126    #[test]
1127    fn test_gctf_document_has_response_error_conflict() {
1128        let mut doc = GctfDocument::new("test.gctf".to_string());
1129        assert!(!doc.has_response_error_conflict());
1130
1131        doc.sections.push(Section {
1132            section_type: SectionType::Response,
1133            content: SectionContent::Json(json!({"result": "ok"})),
1134            inline_options: InlineOptions::default(),
1135            raw_content: "".to_string(),
1136            start_line: 1,
1137            end_line: 2,
1138            attributes: Vec::new(),
1139        });
1140        assert!(!doc.has_response_error_conflict());
1141
1142        doc.sections.push(Section {
1143            section_type: SectionType::Error,
1144            content: SectionContent::Json(json!({"code": 5})),
1145            inline_options: InlineOptions::default(),
1146            raw_content: "".to_string(),
1147            start_line: 3,
1148            end_line: 4,
1149            attributes: Vec::new(),
1150        });
1151        assert!(doc.has_response_error_conflict());
1152    }
1153
1154    #[test]
1155    fn test_inline_options_default() {
1156        let options = InlineOptions::default();
1157        assert!(!options.with_asserts);
1158        assert!(!options.partial);
1159        assert!(options.tolerance.is_none());
1160        assert!(options.redact.is_empty());
1161        assert!(!options.unordered_arrays);
1162    }
1163
1164    #[test]
1165    fn test_section_format_header_with_inline_options() {
1166        let section = Section {
1167            section_type: SectionType::Response,
1168            content: SectionContent::Json(serde_json::json!({"ok": true})),
1169            inline_options: InlineOptions {
1170                with_asserts: true,
1171                partial: true,
1172                tolerance: Some(0.1),
1173                redact: vec!["token".to_string()],
1174                unordered_arrays: true,
1175            },
1176            raw_content: "".to_string(),
1177            start_line: 0,
1178            end_line: 0,
1179            attributes: Vec::new(),
1180        };
1181
1182        let header = section.format_header();
1183        assert_eq!(
1184            header,
1185            "--- RESPONSE partial tolerance=0.1 redact=[\"token\"] unordered_arrays with_asserts ---"
1186        );
1187    }
1188
1189    #[test]
1190    fn test_section_content_debug() {
1191        let content = SectionContent::Single("test".to_string());
1192        let debug_str = format!("{:?}", content);
1193        assert!(debug_str.contains("Single"));
1194    }
1195
1196    #[test]
1197    fn test_section_header_keyword_from_source() {
1198        let section = Section {
1199            section_type: SectionType::Response,
1200            content: SectionContent::Json(serde_json::json!({"ok": true})),
1201            inline_options: InlineOptions::default(),
1202            raw_content: "{\"ok\":true}".to_string(),
1203            start_line: 0,
1204            end_line: 2,
1205            attributes: Vec::new(),
1206        };
1207
1208        let source = "--- RESPONSE with_asserts=true ---\n{\"ok\":true}\n";
1209        assert_eq!(section.header_keyword_from_source(source), Some("RESPONSE"));
1210    }
1211
1212    #[test]
1213    fn test_document_detects_deprecated_headers_alias() {
1214        let mut doc = GctfDocument::new("test.gctf".to_string());
1215        doc.metadata.source = Some("--- HEADERS ---\nAuthorization: Bearer t\n".to_string());
1216        doc.sections.push(Section {
1217            section_type: SectionType::RequestHeaders,
1218            content: SectionContent::KeyValues(HashMap::from([(
1219                "Authorization".to_string(),
1220                "Bearer t".to_string(),
1221            )])),
1222            inline_options: InlineOptions::default(),
1223            raw_content: "Authorization: Bearer t".to_string(),
1224            start_line: 0,
1225            end_line: 2,
1226            attributes: Vec::new(),
1227        });
1228
1229        assert!(doc.section_uses_deprecated_headers_alias(&doc.sections[0]));
1230    }
1231
1232    #[test]
1233    fn test_gctf_document_debug() {
1234        let doc = GctfDocument::new("test.gctf".to_string());
1235        let debug_str = format!("{:?}", doc);
1236        assert!(debug_str.contains("test.gctf"));
1237    }
1238
1239    // ─── Document chain (linked-list) tests ───
1240
1241    #[test]
1242    fn test_document_chain_single() {
1243        let doc = GctfDocument::new("test.gctf".to_string());
1244        assert!(doc.is_single_document());
1245        assert_eq!(doc.document_count(), 1);
1246    }
1247
1248    #[test]
1249    fn test_document_chain_two_docs() {
1250        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1251        let doc2 = GctfDocument::new("test.gctf".to_string());
1252        doc1.next_document = Some(Box::new(doc2));
1253
1254        assert!(!doc1.is_single_document());
1255        assert_eq!(doc1.document_count(), 2);
1256    }
1257
1258    #[test]
1259    fn test_document_chain_three_docs() {
1260        let mut doc3 = GctfDocument::new("test.gctf".to_string());
1261        doc3.file_path = "doc3".to_string();
1262
1263        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1264        doc2.file_path = "doc2".to_string();
1265        doc2.next_document = Some(Box::new(doc3));
1266
1267        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1268        doc1.file_path = "doc1".to_string();
1269        doc1.next_document = Some(Box::new(doc2));
1270
1271        assert_eq!(doc1.document_count(), 3);
1272
1273        let docs: Vec<_> = doc1.iter_chain().collect();
1274        assert_eq!(docs.len(), 3);
1275        assert_eq!(docs[0].file_path, "doc1");
1276        assert_eq!(docs[1].file_path, "doc2");
1277        assert_eq!(docs[2].file_path, "doc3");
1278    }
1279
1280    #[test]
1281    fn test_document_chain_get_document() {
1282        let mut doc2 = GctfDocument::new("test.gctf".to_string());
1283        doc2.file_path = "doc2".to_string();
1284
1285        let mut doc1 = GctfDocument::new("test.gctf".to_string());
1286        doc1.file_path = "doc1".to_string();
1287        doc1.next_document = Some(Box::new(doc2));
1288
1289        assert_eq!(doc1.get_document(0).unwrap().file_path, "doc1");
1290        assert_eq!(doc1.get_document(1).unwrap().file_path, "doc2");
1291        assert!(doc1.get_document(2).is_none());
1292    }
1293
1294    #[test]
1295    fn test_document_chain_iter_on_last() {
1296        let doc = GctfDocument::new("test.gctf".to_string());
1297        let docs: Vec<_> = doc.iter_chain().collect();
1298        assert_eq!(docs.len(), 1);
1299        assert_eq!(docs[0].file_path, "test.gctf");
1300    }
1301
1302    #[test]
1303    fn test_gctf_attribute_parse_u64() {
1304        assert_eq!(GctfAttribute::new("timeout", "30").parse_u64(), Some(30));
1305        assert_eq!(
1306            GctfAttribute::new("timeout", "  30  ").parse_u64(),
1307            Some(30)
1308        );
1309        assert_eq!(GctfAttribute::new("timeout", "0").parse_u64(), Some(0));
1310        assert_eq!(GctfAttribute::new("timeout", "abc").parse_u64(), None);
1311        assert_eq!(GctfAttribute::new("timeout", "-1").parse_u64(), None);
1312    }
1313
1314    #[test]
1315    fn test_gctf_attribute_parse_u32() {
1316        assert_eq!(GctfAttribute::new("retry", "3").parse_u32(), Some(3));
1317        assert_eq!(GctfAttribute::new("retry", "  5  ").parse_u32(), Some(5));
1318        assert_eq!(GctfAttribute::new("retry", "0").parse_u32(), Some(0));
1319        assert_eq!(GctfAttribute::new("retry", "abc").parse_u32(), None);
1320    }
1321
1322    #[test]
1323    fn test_gctf_attribute_parse_f64() {
1324        assert_eq!(
1325            GctfAttribute::new("tolerance", "0.1").parse_f64(),
1326            Some(0.1)
1327        );
1328        assert_eq!(
1329            GctfAttribute::new("tolerance", "  1.5  ").parse_f64(),
1330            Some(1.5)
1331        );
1332        assert_eq!(GctfAttribute::new("tolerance", "abc").parse_f64(), None);
1333    }
1334
1335    #[test]
1336    fn test_gctf_attribute_parse_bool() {
1337        let cases_true = vec!["true", "1", "yes", "on", "True", "TRUE", "YES"];
1338        for v in cases_true {
1339            assert_eq!(
1340                GctfAttribute::new("skip", v).parse_bool(),
1341                Some(true),
1342                "failed for {}",
1343                v
1344            );
1345        }
1346        let cases_false = vec!["false", "0", "no", "off", "", "False", "FALSE"];
1347        for v in cases_false {
1348            assert_eq!(
1349                GctfAttribute::new("skip", v).parse_bool(),
1350                Some(false),
1351                "failed for {}",
1352                v
1353            );
1354        }
1355        assert_eq!(GctfAttribute::new("skip", "maybe").parse_bool(), None);
1356    }
1357
1358    #[test]
1359    fn test_gctf_attribute_as_str() {
1360        assert_eq!(GctfAttribute::new("name", "hello").as_str(), "hello");
1361        assert_eq!(GctfAttribute::flag("skip").as_str(), "true");
1362    }
1363
1364    #[test]
1365    fn test_section_get_attribute() {
1366        let section = Section {
1367            section_type: SectionType::Request,
1368            content: SectionContent::Empty,
1369            inline_options: InlineOptions::default(),
1370            raw_content: String::new(),
1371            start_line: 0,
1372            end_line: 0,
1373            attributes: vec![
1374                GctfAttribute::new("timeout", "30"),
1375                GctfAttribute::new("retry", "2"),
1376            ],
1377        };
1378        assert!(section.get_attribute("timeout").is_some());
1379        assert!(section.get_attribute("retry").is_some());
1380        assert!(section.get_attribute("skip").is_none());
1381        assert_eq!(
1382            section.get_attribute("timeout").unwrap().parse_u64(),
1383            Some(30)
1384        );
1385    }
1386
1387    #[test]
1388    fn test_section_get_timeout() {
1389        let section = Section {
1390            section_type: SectionType::Request,
1391            content: SectionContent::Empty,
1392            inline_options: InlineOptions::default(),
1393            raw_content: String::new(),
1394            start_line: 0,
1395            end_line: 0,
1396            attributes: vec![GctfAttribute::new("timeout", "10")],
1397        };
1398        assert_eq!(section.get_timeout(), Some(10));
1399    }
1400
1401    #[test]
1402    fn test_section_get_timeout_zero() {
1403        let section = Section {
1404            section_type: SectionType::Request,
1405            content: SectionContent::Empty,
1406            inline_options: InlineOptions::default(),
1407            raw_content: String::new(),
1408            start_line: 0,
1409            end_line: 0,
1410            attributes: vec![GctfAttribute::new("timeout", "0")],
1411        };
1412        assert_eq!(section.get_timeout(), None);
1413    }
1414
1415    #[test]
1416    fn test_section_get_timeout_missing() {
1417        let section = Section::default();
1418        assert_eq!(section.get_timeout(), None);
1419    }
1420
1421    #[test]
1422    fn test_section_get_retry() {
1423        let section = Section {
1424            section_type: SectionType::Request,
1425            content: SectionContent::Empty,
1426            inline_options: InlineOptions::default(),
1427            raw_content: String::new(),
1428            start_line: 0,
1429            end_line: 0,
1430            attributes: vec![GctfAttribute::new("retry", "3")],
1431        };
1432        assert_eq!(section.get_retry(), Some(3));
1433    }
1434
1435    #[test]
1436    fn test_section_get_retry_missing() {
1437        let section = Section::default();
1438        assert_eq!(section.get_retry(), None);
1439    }
1440
1441    #[test]
1442    fn test_section_get_skip() {
1443        let section = Section {
1444            section_type: SectionType::Request,
1445            content: SectionContent::Empty,
1446            inline_options: InlineOptions::default(),
1447            raw_content: String::new(),
1448            start_line: 0,
1449            end_line: 0,
1450            attributes: vec![GctfAttribute::flag("skip")],
1451        };
1452        assert!(section.get_skip());
1453    }
1454
1455    #[test]
1456    fn test_section_get_skip_explicit() {
1457        let section = Section {
1458            section_type: SectionType::Request,
1459            content: SectionContent::Empty,
1460            inline_options: InlineOptions::default(),
1461            raw_content: String::new(),
1462            start_line: 0,
1463            end_line: 0,
1464            attributes: vec![GctfAttribute::new("skip", "true")],
1465        };
1466        assert!(section.get_skip());
1467    }
1468
1469    #[test]
1470    fn test_section_get_skip_false() {
1471        let section = Section::default();
1472        assert!(!section.get_skip());
1473    }
1474
1475    #[test]
1476    fn test_section_has_tag() {
1477        let section = Section {
1478            section_type: SectionType::Request,
1479            content: SectionContent::Empty,
1480            inline_options: InlineOptions::default(),
1481            raw_content: String::new(),
1482            start_line: 0,
1483            end_line: 0,
1484            attributes: vec![GctfAttribute::new("tag", "smoke,slow")],
1485        };
1486        assert!(section.has_tag("smoke"));
1487        assert!(section.has_tag("slow"));
1488        assert!(!section.has_tag("integration"));
1489    }
1490
1491    #[test]
1492    fn test_section_has_tag_single() {
1493        let section = Section {
1494            section_type: SectionType::Request,
1495            content: SectionContent::Empty,
1496            inline_options: InlineOptions::default(),
1497            raw_content: String::new(),
1498            start_line: 0,
1499            end_line: 0,
1500            attributes: vec![GctfAttribute::new("tag", "smoke")],
1501        };
1502        assert!(section.has_tag("smoke"));
1503        assert!(!section.has_tag("slow"));
1504    }
1505
1506    #[test]
1507    fn test_section_has_tag_missing() {
1508        let section = Section::default();
1509        assert!(!section.has_tag("smoke"));
1510    }
1511}