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

gripmock / grpctestify-rust / 24816969627

23 Apr 2026 04:35AM UTC coverage: 76.897% (+0.3%) from 76.58%
24816969627

push

github

web-flow
Merge pull request #41 from gripmock/err-msg-fix

feat: ERROR section should support partial and with_asserts inline options

463 of 609 new or added lines in 6 files covered. (76.03%)

13 existing lines in 2 files now uncovered.

18103 of 23542 relevant lines covered (76.9%)

2375.92 hits per line

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

87.73
/src/parser/validator.rs
1
// GCTF document validator - validates parsed AST
2
// Checks for required sections, conflicts, and data integrity
3

4
use super::ast::*;
5
use anyhow::{Result, bail};
6
use serde::Serialize;
7

8
/// Validation error
9
#[derive(Debug, Clone, Serialize)]
10
pub struct ValidationError {
11
    pub message: String,
12
    pub line: Option<usize>,
13
    pub severity: ErrorSeverity,
14
}
15

16
/// Error severity
17
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18
#[serde(rename_all = "lowercase")]
19
pub enum ErrorSeverity {
20
    Error,
21
    Warning,
22
    Info,
23
}
24

25
/// Validate a parsed GCTF document (returns all errors/warnings without bailing)
26
pub fn validate_document_diagnostics(document: &GctfDocument) -> Vec<ValidationError> {
10,068✔
27
    let mut errors = Vec::new();
10,068✔
28

29
    // Check for required sections
30
    validate_required_sections(document, &mut errors);
10,068✔
31

32
    // Check for conflicts
33
    validate_conflicts(document, &mut errors);
10,068✔
34

35
    // Validate content
36
    validate_content(document, &mut errors);
10,068✔
37

38
    // Validate structure
39
    validate_structure(document, &mut errors);
10,068✔
40

41
    errors
10,068✔
42
}
10,068✔
43

44
/// Validate a parsed GCTF document (legacy wrapper that bails on error)
45
pub fn validate_document(document: &GctfDocument) -> Result<Vec<ValidationError>> {
10,051✔
46
    let errors = validate_document_diagnostics(document);
10,051✔
47
    let has_errors = errors.iter().any(|e| e.severity == ErrorSeverity::Error);
20,044✔
48

49
    if has_errors {
10,051✔
50
        let error_messages: Vec<String> = errors
10,003✔
51
            .iter()
10,003✔
52
            .filter(|e| e.severity == ErrorSeverity::Error)
20,005✔
53
            .map(|e| format!("Line {}: {}", e.line.unwrap_or(0), e.message))
10,005✔
54
            .collect();
10,003✔
55

56
        bail!("Validation failed:\n{}", error_messages.join("\n"));
10,003✔
57
    }
48✔
58

59
    Ok(errors)
48✔
60
}
10,051✔
61

62
/// Validate required sections
63
fn validate_required_sections(document: &GctfDocument, errors: &mut Vec<ValidationError>) {
10,068✔
64
    // ENDPOINT is required
65
    if document.get_endpoint().is_none() {
10,068✔
66
        errors.push(ValidationError {
1✔
67
            message: "ENDPOINT section is required".to_string(),
1✔
68
            line: None,
1✔
69
            severity: ErrorSeverity::Error,
1✔
70
        });
1✔
71
    }
10,067✔
72

73
    // ADDRESS or environment variable is required
74
    // The address might also be provided via CLI args or Config file, which the validator
75
    // doesn't have access to here. We should probably relax this check or make it a warning.
76
    // Ideally validation happens with full context, but for now let's check env var.
77
    // If neither section nor env var is present, we warn instead of error,
78
    // because it might be supplied at runtime.
79
    let env_addr = std::env::var(crate::config::ENV_GRPCTESTIFY_ADDRESS).ok();
10,068✔
80
    if document.get_address(env_addr.as_deref()).is_none() {
10,068✔
81
        // Downgrade to warning because address can come from CLI/Config
10,041✔
82
        errors.push(ValidationError {
10,041✔
83
            message: format!(
10,041✔
84
                "ADDRESS section missing (ensure {} is set or passed via --address)",
10,041✔
85
                crate::config::ENV_GRPCTESTIFY_ADDRESS
10,041✔
86
            ),
10,041✔
87
            line: None,
10,041✔
88
            severity: ErrorSeverity::Warning,
10,041✔
89
        });
10,041✔
90
    }
10,041✔
91

92
    // At least RESPONSE, ERROR or ASSERTS should be present for verification
93
    let has_response = document.first_section(SectionType::Response).is_some();
10,068✔
94
    let has_error = document.first_section(SectionType::Error).is_some();
10,068✔
95
    let has_asserts = document.first_section(SectionType::Asserts).is_some();
10,068✔
96

97
    if !has_response && !has_error && !has_asserts {
10,068✔
98
        errors.push(ValidationError {
5✔
99
            message: "At least one verification section (RESPONSE, ERROR, or ASSERTS) is required"
5✔
100
                .to_string(),
5✔
101
            line: None,
5✔
102
            severity: ErrorSeverity::Error,
5✔
103
        });
5✔
104
    }
10,063✔
105
}
10,068✔
106

107
/// Validate conflicts
108
fn validate_conflicts(document: &GctfDocument, errors: &mut Vec<ValidationError>) {
10,068✔
109
    // RESPONSE and ERROR cannot both be present
110
    if document.has_response_error_conflict() {
10,068✔
111
        errors.push(ValidationError {
1✔
112
            message: "Cannot have both RESPONSE and ERROR sections".to_string(),
1✔
113
            line: None,
1✔
114
            severity: ErrorSeverity::Error,
1✔
115
        });
1✔
116
    }
10,067✔
117
}
10,068✔
118

119
/// Validate content
120
fn validate_content(document: &GctfDocument, errors: &mut Vec<ValidationError>) {
10,068✔
121
    // Validate endpoint format
122
    if let Some(endpoint) = document.get_endpoint()
10,068✔
123
        && !endpoint.contains('/')
10,067✔
124
    {
125
        errors.push(ValidationError {
10,001✔
126
            message: format!(
10,001✔
127
                "Invalid endpoint format: {}. Expected format: package.Service/Method",
128
                endpoint
129
            ),
130
            line: document
10,001✔
131
                .first_section(SectionType::Endpoint)
10,001✔
132
                .map(|s| s.start_line),
10,001✔
133
            severity: ErrorSeverity::Error,
10,001✔
134
        });
135
    }
67✔
136

137
    // Validate address format
138
    if let Some(address) = document.get_address(None)
10,068✔
139
        && !address.contains(':')
26✔
140
    {
141
        errors.push(ValidationError {
1✔
142
            message: format!(
1✔
143
                "Invalid address format: {}. Expected format: host:port",
144
                address
145
            ),
146
            line: document
1✔
147
                .first_section(SectionType::Address)
1✔
148
                .map(|s| s.start_line),
1✔
149
            severity: ErrorSeverity::Error,
1✔
150
        });
151
    }
10,067✔
152

153
    // Validate JSON sections
154
    for section_type in [
30,204✔
155
        SectionType::Request,
10,068✔
156
        SectionType::Response,
10,068✔
157
        SectionType::Error,
10,068✔
158
    ] {
10,068✔
159
        for section in document.sections_by_type(section_type) {
30,204✔
160
            match &section.content {
20,106✔
161
                SectionContent::Json(json) => {
20,101✔
162
                    // Check if JSON is valid object or array
163
                    // For ERROR, we also allow strings
164
                    let is_valid = if section_type == SectionType::Error {
20,101✔
165
                        json.is_object() || json.is_array() || json.is_string()
7✔
166
                    } else {
167
                        json.is_object() || json.is_array()
20,094✔
168
                    };
169

170
                    if !is_valid {
20,101✔
171
                        errors.push(ValidationError {
×
172
                            message: format!(
×
173
                                "{:?} section must contain valid JSON object or array{}",
174
                                section_type,
175
                                if section_type == SectionType::Error {
×
176
                                    " or string"
×
177
                                } else {
178
                                    ""
×
179
                                }
180
                            ),
181
                            line: Some(section.start_line),
×
182
                            severity: ErrorSeverity::Error,
×
183
                        });
184
                    }
20,101✔
185

186
                    if section_type == SectionType::Error
20,101✔
187
                        && let Some(details) = json.get("details")
7✔
188
                    {
189
                        if !details.is_array() {
2✔
190
                            errors.push(ValidationError {
1✔
191
                                message: "ERROR section field 'details' must be an array"
1✔
192
                                    .to_string(),
1✔
193
                                line: Some(section.start_line),
1✔
194
                                severity: ErrorSeverity::Error,
1✔
195
                            });
1✔
196
                        } else if let Some(detail_items) = details.as_array() {
1✔
197
                            for detail in detail_items {
1✔
198
                                if !detail.is_object() {
1✔
199
                                    errors.push(ValidationError {
1✔
200
                                        message: "ERROR section 'details' items must be objects"
1✔
201
                                            .to_string(),
1✔
202
                                        line: Some(section.start_line),
1✔
203
                                        severity: ErrorSeverity::Error,
1✔
204
                                    });
1✔
205
                                    break;
1✔
206
                                }
×
207

208
                                if let Some(type_value) = detail.get("@type")
×
209
                                    && !type_value.is_string()
×
210
                                {
×
211
                                    errors.push(ValidationError {
×
212
                                        message:
×
213
                                            "ERROR.details item field '@type' must be a string"
×
214
                                                .to_string(),
×
215
                                        line: Some(section.start_line),
×
216
                                        severity: ErrorSeverity::Error,
×
217
                                    });
×
218
                                }
×
219
                            }
220
                        }
×
221
                    }
20,099✔
222
                }
223
                SectionContent::JsonLines(values) => {
1✔
224
                    if section_type != SectionType::Response {
1✔
225
                        errors.push(ValidationError {
×
226
                            message: format!(
×
227
                                "{:?} section does not support newline-delimited JSON messages",
×
228
                                section_type
×
229
                            ),
×
230
                            line: Some(section.start_line),
×
231
                            severity: ErrorSeverity::Error,
×
232
                        });
×
233
                    } else if values.is_empty() {
1✔
234
                        errors.push(ValidationError {
×
235
                            message: "RESPONSE section contains no JSON messages".to_string(),
×
236
                            line: Some(section.start_line),
×
237
                            severity: ErrorSeverity::Error,
×
238
                        });
×
239
                    }
1✔
240
                }
241
                _ => {}
4✔
242
            }
243
        }
244
    }
245

246
    // Validate key-value sections
247
    for section_type in [
40,272✔
248
        SectionType::RequestHeaders,
10,068✔
249
        SectionType::Tls,
10,068✔
250
        SectionType::Proto,
10,068✔
251
        SectionType::Options,
10,068✔
252
    ] {
10,068✔
253
        for section in document.sections_by_type(section_type) {
40,272✔
254
            if let SectionContent::KeyValues(kv) = &section.content {
10✔
255
                // Check for empty keys or values
256
                for key in kv.keys() {
19✔
257
                    if key.is_empty() {
19✔
258
                        errors.push(ValidationError {
×
259
                            message: format!("Empty key in {:?} section", section_type),
×
260
                            line: Some(section.start_line),
×
261
                            severity: ErrorSeverity::Error,
×
262
                        });
×
263
                    }
19✔
264
                }
265

266
                if section_type == SectionType::Options {
10✔
267
                    for (key, value) in kv {
19✔
268
                        match key.as_str() {
19✔
269
                            "timeout" => {
19✔
270
                                if value.trim().parse::<u64>().ok().is_none_or(|v| v == 0) {
7✔
271
                                    errors.push(ValidationError {
1✔
272
                                        message: format!(
1✔
273
                                            "OPTIONS.timeout must be a positive integer, got '{}'",
1✔
274
                                            value
1✔
275
                                        ),
1✔
276
                                        line: Some(section.start_line),
1✔
277
                                        severity: ErrorSeverity::Error,
1✔
278
                                    });
1✔
279
                                }
6✔
280
                            }
281
                            "no-retry" | "no_retry" => {
12✔
282
                                let normalized = value.trim().to_ascii_lowercase();
1✔
283
                                let is_bool = matches!(
1✔
284
                                    normalized.as_str(),
1✔
285
                                    "true" | "1" | "yes" | "on" | "false" | "0" | "no" | "off"
1✔
286
                                );
287
                                if !is_bool {
1✔
288
                                    errors.push(ValidationError {
×
289
                                        message: format!(
×
290
                                            "OPTIONS.{} must be a boolean, got '{}'",
×
291
                                            key, value
×
292
                                        ),
×
293
                                        line: Some(section.start_line),
×
294
                                        severity: ErrorSeverity::Error,
×
295
                                    });
×
296
                                }
1✔
297
                            }
298
                            "retry" => {
11✔
299
                                if value.trim().parse::<u32>().is_err() {
6✔
300
                                    errors.push(ValidationError {
×
301
                                        message: format!(
×
302
                                            "OPTIONS.retry must be a non-negative integer, got '{}'",
×
303
                                            value
×
304
                                        ),
×
305
                                        line: Some(section.start_line),
×
306
                                        severity: ErrorSeverity::Error,
×
307
                                    });
×
308
                                }
6✔
309
                            }
310
                            "retry-delay" | "retry_delay" => {
5✔
311
                                if value.trim().parse::<f64>().ok().is_none_or(|v| v < 0.0) {
1✔
312
                                    errors.push(ValidationError {
×
313
                                        message: format!(
×
314
                                            "OPTIONS.retry-delay must be a non-negative number, got '{}'",
×
315
                                            value
×
316
                                        ),
×
317
                                        line: Some(section.start_line),
×
318
                                        severity: ErrorSeverity::Error,
×
319
                                    });
×
320
                                }
1✔
321
                            }
322
                            "compression" => {
4✔
323
                                let normalized = value.trim().to_ascii_lowercase();
2✔
324
                                if !matches!(normalized.as_str(), "none" | "gzip") {
2✔
325
                                    errors.push(ValidationError {
1✔
326
                                        message: format!(
1✔
327
                                            "OPTIONS.compression must be one of: none, gzip (got '{}')",
1✔
328
                                            value
1✔
329
                                        ),
1✔
330
                                        line: Some(section.start_line),
1✔
331
                                        severity: ErrorSeverity::Error,
1✔
332
                                    });
1✔
333
                                }
1✔
334
                            }
335
                            _ => {
2✔
336
                                errors.push(ValidationError {
2✔
337
                                    message: format!(
2✔
338
                                        "Unknown OPTIONS key '{}'. Supported keys: timeout, retry, retry-delay, no-retry, compression",
2✔
339
                                        key
2✔
340
                                    ),
2✔
341
                                    line: Some(section.start_line),
2✔
342
                                    severity: ErrorSeverity::Warning,
2✔
343
                                });
2✔
344
                            }
2✔
345
                        }
346
                    }
347
                }
×
348
            }
×
349
        }
350
    }
351

352
    // Validate assertions
353
    for section in document.sections_by_type(SectionType::Asserts) {
10,068✔
354
        if let SectionContent::Assertions(assertions) = &section.content {
9✔
355
            for assertion in assertions {
9✔
356
                if assertion.is_empty() {
9✔
357
                    errors.push(ValidationError {
×
358
                        message: "Empty assertion found".to_string(),
×
359
                        line: Some(section.start_line),
×
360
                        severity: ErrorSeverity::Warning,
×
361
                    });
×
362
                }
9✔
363
            }
364
        }
×
365
    }
366
}
10,068✔
367

368
/// Validate structure
369
fn validate_structure(document: &GctfDocument, errors: &mut Vec<ValidationError>) {
10,068✔
370
    // Check for duplicate non-multiple sections
371
    let mut seen_sections = std::collections::HashSet::new();
10,068✔
372
    let mut meta_count = 0;
10,068✔
373
    let mut meta_first_line = None;
10,068✔
374

375
    for section in &document.sections {
33,618✔
376
        if section.section_type == SectionType::Meta {
33,618✔
377
            meta_count += 1;
×
378
            if meta_first_line.is_none() {
×
379
                meta_first_line = Some(section.start_line);
×
380
            }
×
381
        }
33,618✔
382

383
        if !section.section_type.is_multiple_allowed() {
33,618✔
384
            if seen_sections.contains(&section.section_type) {
10,113✔
385
                errors.push(ValidationError {
×
386
                    message: format!("Duplicate {:?} section found", section.section_type),
×
387
                    line: Some(section.start_line),
×
388
                    severity: ErrorSeverity::Error,
×
389
                });
×
390
            }
10,113✔
391
            seen_sections.insert(section.section_type);
10,113✔
392
        }
23,505✔
393
    }
394

395
    // Validate META section: only 0 or 1 per file, must be first if present
396
    if meta_count > 1 {
10,068✔
397
        errors.push(ValidationError {
×
398
            message: "Only one META section is allowed per file".to_string(),
×
399
            line: meta_first_line,
×
400
            severity: ErrorSeverity::Error,
×
401
        });
×
402
    }
10,068✔
403

404
    // Check META is first section (if present)
405
    if meta_count == 1
10,068✔
406
        && let Some(first_section) = document.sections.first()
×
407
        && first_section.section_type != SectionType::Meta
×
408
    {
×
409
        errors.push(ValidationError {
×
410
            message: "META section must be the first section in the file".to_string(),
×
411
            line: meta_first_line,
×
412
            severity: ErrorSeverity::Error,
×
413
        });
×
414
    }
10,068✔
415

416
    // Validate section order (optional, but good for readability)
417
    // Not enforcing strict order, just checking for obvious issues
418
    // TODO: Add optional strict ordering validation
419

420
    // Validate inline options are only on supported sections
421
    for section in &document.sections {
33,618✔
422
        let has_any_inline_options = section.inline_options.with_asserts
33,618✔
423
            || section.inline_options.partial
33,615✔
424
            || section.inline_options.tolerance.is_some()
33,613✔
425
            || !section.inline_options.redact.is_empty()
33,612✔
426
            || section.inline_options.unordered_arrays;
33,612✔
427

428
        if !has_any_inline_options {
33,618✔
429
            continue;
33,612✔
430
        }
6✔
431

432
        match section.section_type {
6✔
433
            SectionType::Response => {
1✔
434
                // All known options are supported for RESPONSE section
1✔
435
            }
1✔
436
            SectionType::Error => {
437
                if section.inline_options.tolerance.is_some()
5✔
438
                    || !section.inline_options.redact.is_empty()
4✔
439
                    || section.inline_options.unordered_arrays
4✔
440
                {
1✔
441
                    errors.push(ValidationError {
1✔
442
                        message:
1✔
443
                            "ERROR section only supports partial and with_asserts inline options"
1✔
444
                                .to_string(),
1✔
445
                        line: Some(section.start_line),
1✔
446
                        severity: ErrorSeverity::Warning,
1✔
447
                    });
1✔
448
                }
4✔
449
            }
450
            _ => {
×
451
                errors.push(ValidationError {
×
452
                    message: format!(
×
453
                        "Inline options are not supported for {:?} section",
×
454
                        section.section_type
×
455
                    ),
×
456
                    line: Some(section.start_line),
×
457
                    severity: ErrorSeverity::Warning,
×
458
                });
×
459
            }
×
460
        }
461
    }
462

463
    // Warn about redundant empty ERROR with with_asserts
464
    for (i, section) in document.sections.iter().enumerate() {
33,618✔
465
        if section.section_type == SectionType::Error
33,618✔
466
            && section.inline_options.with_asserts
10✔
467
            && matches!(section.content, SectionContent::Empty)
3✔
468
            && document
2✔
469
                .sections
2✔
470
                .get(i + 1)
2✔
471
                .is_some_and(|next| next.section_type == SectionType::Asserts)
2✔
472
        {
1✔
473
            errors.push(ValidationError {
1✔
474
                message:
1✔
475
                    "Empty ERROR with with_asserts is redundant; remove ERROR and keep ASSERTS"
1✔
476
                        .to_string(),
1✔
477
                line: Some(section.start_line),
1✔
478
                severity: ErrorSeverity::Warning,
1✔
479
            });
1✔
480
        }
33,617✔
481
    }
482
}
10,068✔
483

484
/// Check if validation passed (no errors)
485
pub fn validation_passed(errors: &[ValidationError]) -> bool {
2✔
486
    !errors.iter().any(|e| e.severity == ErrorSeverity::Error)
4✔
487
}
2✔
488

489
#[cfg(test)]
490
mod tests {
491
    use super::*;
492

493
    fn create_test_document() -> GctfDocument {
25✔
494
        let mut doc = GctfDocument::new("test.gctf".to_string());
25✔
495

496
        doc.sections = vec![
25✔
497
            Section {
25✔
498
                section_type: SectionType::Address,
25✔
499
                content: SectionContent::Single("localhost:4770".to_string()),
25✔
500
                inline_options: InlineOptions::default(),
25✔
501
                raw_content: "localhost:4770".to_string(),
25✔
502
                start_line: 1,
25✔
503
                end_line: 1,
25✔
504
            },
25✔
505
            Section {
25✔
506
                section_type: SectionType::Endpoint,
25✔
507
                content: SectionContent::Single("my.Service/Method".to_string()),
25✔
508
                inline_options: InlineOptions::default(),
25✔
509
                raw_content: "my.Service/Method".to_string(),
25✔
510
                start_line: 3,
25✔
511
                end_line: 3,
25✔
512
            },
25✔
513
        ];
514

515
        doc
25✔
516
    }
25✔
517

518
    #[test]
519
    fn test_validate_required_sections_pass() {
1✔
520
        let doc = create_test_document();
1✔
521
        let result = validate_document(&doc);
1✔
522
        // Should fail because no REQUEST or ASSERTS
523
        assert!(result.is_err());
1✔
524
    }
1✔
525

526
    #[test]
527
    fn test_validate_endpoint_format() {
1✔
528
        let mut doc = create_test_document();
1✔
529
        doc.sections[1].content = SectionContent::Single("invalid_endpoint".to_string());
1✔
530

531
        let result = validate_document(&doc);
1✔
532
        assert!(result.is_err());
1✔
533
    }
1✔
534

535
    #[test]
536
    fn test_validate_address_format() {
1✔
537
        let mut doc = create_test_document();
1✔
538
        doc.sections[0].content = SectionContent::Single("invalid_address".to_string());
1✔
539

540
        let result = validate_document(&doc);
1✔
541
        assert!(result.is_err());
1✔
542
    }
1✔
543

544
    #[test]
545
    fn test_validation_passed() {
1✔
546
        let errors = vec![
1✔
547
            ValidationError {
1✔
548
                message: "Warning".to_string(),
1✔
549
                line: Some(1),
1✔
550
                severity: ErrorSeverity::Warning,
1✔
551
            },
1✔
552
            ValidationError {
1✔
553
                message: "Info".to_string(),
1✔
554
                line: Some(2),
1✔
555
                severity: ErrorSeverity::Info,
1✔
556
            },
1✔
557
        ];
558

559
        assert!(validation_passed(&errors));
1✔
560
    }
1✔
561

562
    #[test]
563
    fn test_validation_failed() {
1✔
564
        let errors = vec![
1✔
565
            ValidationError {
1✔
566
                message: "Warning".to_string(),
1✔
567
                line: Some(1),
1✔
568
                severity: ErrorSeverity::Warning,
1✔
569
            },
1✔
570
            ValidationError {
1✔
571
                message: "Error".to_string(),
1✔
572
                line: Some(2),
1✔
573
                severity: ErrorSeverity::Error,
1✔
574
            },
1✔
575
        ];
576

577
        assert!(!validation_passed(&errors));
1✔
578
    }
1✔
579

580
    #[test]
581
    fn test_validate_document_diagnostics() {
1✔
582
        let doc = create_test_document();
1✔
583
        let errors = validate_document_diagnostics(&doc);
1✔
584
        // Should have some errors or warnings
585
        assert!(!errors.is_empty());
1✔
586
    }
1✔
587

588
    #[test]
589
    fn test_validate_document_with_response() {
1✔
590
        let mut doc = create_test_document();
1✔
591
        doc.sections.push(Section {
1✔
592
            section_type: SectionType::Response,
1✔
593
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
594
            inline_options: InlineOptions::default(),
1✔
595
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
596
            start_line: 5,
1✔
597
            end_line: 6,
1✔
598
        });
1✔
599

600
        let result = validate_document(&doc);
1✔
601
        // Should pass with ADDRESS, ENDPOINT, and RESPONSE
602
        assert!(result.is_ok());
1✔
603
    }
1✔
604

605
    #[test]
606
    fn test_validate_document_with_error_section() {
1✔
607
        let mut doc = create_test_document();
1✔
608
        doc.sections.push(Section {
1✔
609
            section_type: SectionType::Error,
1✔
610
            content: SectionContent::Json(serde_json::json!({"code": 5})),
1✔
611
            inline_options: InlineOptions::default(),
1✔
612
            raw_content: "{\"code\": 5}".to_string(),
1✔
613
            start_line: 5,
1✔
614
            end_line: 6,
1✔
615
        });
1✔
616

617
        let result = validate_document(&doc);
1✔
618
        // Should pass with ADDRESS, ENDPOINT, and ERROR
619
        assert!(result.is_ok());
1✔
620
    }
1✔
621

622
    #[test]
623
    fn test_validate_document_error_partial_option_allowed() {
1✔
624
        let mut doc = create_test_document();
1✔
625
        doc.sections.push(Section {
1✔
626
            section_type: SectionType::Error,
1✔
627
            content: SectionContent::Json(serde_json::json!({"code": 5})),
1✔
628
            inline_options: InlineOptions {
1✔
629
                partial: true,
1✔
630
                ..InlineOptions::default()
1✔
631
            },
1✔
632
            raw_content: "{\"code\": 5}".to_string(),
1✔
633
            start_line: 5,
1✔
634
            end_line: 6,
1✔
635
        });
1✔
636

637
        let errors = validate_document_diagnostics(&doc);
1✔
638
        assert!(!errors.iter().any(|e| {
1✔
NEW
639
            e.message
×
NEW
640
                .contains("ERROR section only supports partial and with_asserts")
×
NEW
641
        }));
×
642
    }
1✔
643

644
    #[test]
645
    fn test_validate_document_error_tolerance_still_warns() {
1✔
646
        let mut doc = create_test_document();
1✔
647
        doc.sections.push(Section {
1✔
648
            section_type: SectionType::Error,
1✔
649
            content: SectionContent::Json(serde_json::json!({"code": 5})),
1✔
650
            inline_options: InlineOptions {
1✔
651
                tolerance: Some(0.1),
1✔
652
                ..InlineOptions::default()
1✔
653
            },
1✔
654
            raw_content: "{\"code\": 5}".to_string(),
1✔
655
            start_line: 5,
1✔
656
            end_line: 6,
1✔
657
        });
1✔
658

659
        let errors = validate_document_diagnostics(&doc);
1✔
660
        assert!(errors.iter().any(|e| {
1✔
661
            e.message
1✔
662
                .contains("ERROR section only supports partial and with_asserts")
1✔
663
        }));
1✔
664
    }
1✔
665

666
    #[test]
667
    fn test_validate_document_warns_on_empty_error_with_asserts() {
1✔
668
        let mut doc = create_test_document();
1✔
669
        doc.sections.push(Section {
1✔
670
            section_type: SectionType::Error,
1✔
671
            content: SectionContent::Empty,
1✔
672
            inline_options: InlineOptions {
1✔
673
                with_asserts: true,
1✔
674
                ..InlineOptions::default()
1✔
675
            },
1✔
676
            raw_content: "".to_string(),
1✔
677
            start_line: 5,
1✔
678
            end_line: 5,
1✔
679
        });
1✔
680
        doc.sections.push(Section {
1✔
681
            section_type: SectionType::Asserts,
1✔
682
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
683
            inline_options: InlineOptions::default(),
1✔
684
            raw_content: ".code == 5".to_string(),
1✔
685
            start_line: 6,
1✔
686
            end_line: 6,
1✔
687
        });
1✔
688

689
        let errors = validate_document_diagnostics(&doc);
1✔
690
        assert!(errors.iter().any(|e| {
1✔
691
            e.message
1✔
692
                .contains("Empty ERROR with with_asserts is redundant")
1✔
693
                && e.severity == ErrorSeverity::Warning
1✔
694
        }));
1✔
695
    }
1✔
696

697
    #[test]
698
    fn test_validate_document_no_warning_for_non_empty_error_with_asserts() {
1✔
699
        let mut doc = create_test_document();
1✔
700
        doc.sections.push(Section {
1✔
701
            section_type: SectionType::Error,
1✔
702
            content: SectionContent::Json(serde_json::json!({"code": 5})),
1✔
703
            inline_options: InlineOptions {
1✔
704
                with_asserts: true,
1✔
705
                ..InlineOptions::default()
1✔
706
            },
1✔
707
            raw_content: "{\"code\": 5}".to_string(),
1✔
708
            start_line: 5,
1✔
709
            end_line: 6,
1✔
710
        });
1✔
711
        doc.sections.push(Section {
1✔
712
            section_type: SectionType::Asserts,
1✔
713
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
714
            inline_options: InlineOptions::default(),
1✔
715
            raw_content: ".code == 5".to_string(),
1✔
716
            start_line: 7,
1✔
717
            end_line: 7,
1✔
718
        });
1✔
719

720
        let errors = validate_document_diagnostics(&doc);
1✔
721
        assert!(!errors.iter().any(|e| {
1✔
NEW
722
            e.message
×
NEW
723
                .contains("Empty ERROR with with_asserts is redundant")
×
NEW
724
        }));
×
725
    }
1✔
726

727
    #[test]
728
    fn test_validate_document_no_warning_for_empty_error_without_with_asserts() {
1✔
729
        let mut doc = create_test_document();
1✔
730
        doc.sections.push(Section {
1✔
731
            section_type: SectionType::Error,
1✔
732
            content: SectionContent::Empty,
1✔
733
            inline_options: InlineOptions::default(),
1✔
734
            raw_content: "".to_string(),
1✔
735
            start_line: 5,
1✔
736
            end_line: 5,
1✔
737
        });
1✔
738
        doc.sections.push(Section {
1✔
739
            section_type: SectionType::Asserts,
1✔
740
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
741
            inline_options: InlineOptions::default(),
1✔
742
            raw_content: ".code == 5".to_string(),
1✔
743
            start_line: 6,
1✔
744
            end_line: 6,
1✔
745
        });
1✔
746

747
        let errors = validate_document_diagnostics(&doc);
1✔
748
        assert!(!errors.iter().any(|e| {
1✔
NEW
749
            e.message
×
NEW
750
                .contains("Empty ERROR with with_asserts is redundant")
×
NEW
751
        }));
×
752
    }
1✔
753

754
    #[test]
755
    fn test_validate_document_no_warning_for_empty_error_with_non_adjacent_asserts() {
1✔
756
        let mut doc = create_test_document();
1✔
757
        doc.sections.push(Section {
1✔
758
            section_type: SectionType::Error,
1✔
759
            content: SectionContent::Empty,
1✔
760
            inline_options: InlineOptions {
1✔
761
                with_asserts: true,
1✔
762
                ..InlineOptions::default()
1✔
763
            },
1✔
764
            raw_content: "".to_string(),
1✔
765
            start_line: 5,
1✔
766
            end_line: 5,
1✔
767
        });
1✔
768
        doc.sections.push(Section {
1✔
769
            section_type: SectionType::Request,
1✔
770
            content: SectionContent::Json(serde_json::json!({"id": 1})),
1✔
771
            inline_options: InlineOptions::default(),
1✔
772
            raw_content: "{\"id\": 1}".to_string(),
1✔
773
            start_line: 6,
1✔
774
            end_line: 7,
1✔
775
        });
1✔
776
        doc.sections.push(Section {
1✔
777
            section_type: SectionType::Asserts,
1✔
778
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
779
            inline_options: InlineOptions::default(),
1✔
780
            raw_content: ".code == 5".to_string(),
1✔
781
            start_line: 8,
1✔
782
            end_line: 8,
1✔
783
        });
1✔
784

785
        let errors = validate_document_diagnostics(&doc);
1✔
786
        assert!(!errors.iter().any(|e| {
1✔
NEW
787
            e.message
×
NEW
788
                .contains("Empty ERROR with with_asserts is redundant")
×
NEW
789
        }));
×
790
    }
1✔
791

792
    #[test]
793
    fn test_validate_document_with_asserts() {
1✔
794
        let mut doc = create_test_document();
1✔
795
        doc.sections.push(Section {
1✔
796
            section_type: SectionType::Asserts,
1✔
797
            content: SectionContent::Assertions(vec![".id == 1".to_string()]),
1✔
798
            inline_options: InlineOptions::default(),
1✔
799
            raw_content: ".id == 1".to_string(),
1✔
800
            start_line: 5,
1✔
801
            end_line: 5,
1✔
802
        });
1✔
803

804
        let result = validate_document(&doc);
1✔
805
        // Should pass with ADDRESS, ENDPOINT, and ASSERTS
806
        assert!(result.is_ok());
1✔
807
    }
1✔
808

809
    #[test]
810
    fn test_validate_document_missing_endpoint() {
1✔
811
        let mut doc = create_test_document();
1✔
812
        doc.sections.remove(1); // Remove ENDPOINT
1✔
813

814
        let errors = validate_document_diagnostics(&doc);
1✔
815
        let has_endpoint_error = errors.iter().any(|e| e.message.contains("ENDPOINT"));
1✔
816
        assert!(has_endpoint_error);
1✔
817
    }
1✔
818

819
    #[test]
820
    fn test_validate_document_response_error_conflict() {
1✔
821
        let mut doc = create_test_document();
1✔
822
        doc.sections.push(Section {
1✔
823
            section_type: SectionType::Response,
1✔
824
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
825
            inline_options: InlineOptions::default(),
1✔
826
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
827
            start_line: 5,
1✔
828
            end_line: 6,
1✔
829
        });
1✔
830
        doc.sections.push(Section {
1✔
831
            section_type: SectionType::Error,
1✔
832
            content: SectionContent::Json(serde_json::json!({"code": 5})),
1✔
833
            inline_options: InlineOptions::default(),
1✔
834
            raw_content: "{\"code\": 5}".to_string(),
1✔
835
            start_line: 7,
1✔
836
            end_line: 8,
1✔
837
        });
1✔
838

839
        let errors = validate_document_diagnostics(&doc);
1✔
840
        let has_conflict_error = errors
1✔
841
            .iter()
1✔
842
            .any(|e| e.message.contains("RESPONSE") && e.message.contains("ERROR"));
1✔
843
        assert!(has_conflict_error);
1✔
844
    }
1✔
845

846
    #[test]
847
    fn test_validate_document_empty_requests() {
1✔
848
        let mut doc = create_test_document();
1✔
849
        doc.sections.push(Section {
1✔
850
            section_type: SectionType::Request,
1✔
851
            content: SectionContent::Empty,
1✔
852
            inline_options: InlineOptions::default(),
1✔
853
            raw_content: "".to_string(),
1✔
854
            start_line: 5,
1✔
855
            end_line: 5,
1✔
856
        });
1✔
857
        doc.sections.push(Section {
1✔
858
            section_type: SectionType::Response,
1✔
859
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
860
            inline_options: InlineOptions::default(),
1✔
861
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
862
            start_line: 6,
1✔
863
            end_line: 7,
1✔
864
        });
1✔
865

866
        let result = validate_document(&doc);
1✔
867
        // Empty REQUEST is allowed
868
        assert!(result.is_ok());
1✔
869
    }
1✔
870

871
    #[test]
872
    fn test_validate_document_invalid_request_json() {
1✔
873
        let mut doc = create_test_document();
1✔
874
        doc.sections.push(Section {
1✔
875
            section_type: SectionType::Request,
1✔
876
            content: SectionContent::Json(serde_json::json!({"key": "value"})),
1✔
877
            inline_options: InlineOptions::default(),
1✔
878
            raw_content: "{\"key\": \"value\"}".to_string(),
1✔
879
            start_line: 5,
1✔
880
            end_line: 6,
1✔
881
        });
1✔
882
        doc.sections.push(Section {
1✔
883
            section_type: SectionType::Response,
1✔
884
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
885
            inline_options: InlineOptions::default(),
1✔
886
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
887
            start_line: 7,
1✔
888
            end_line: 8,
1✔
889
        });
1✔
890

891
        let result = validate_document(&doc);
1✔
892
        // Valid JSON should pass
893
        assert!(result.is_ok());
1✔
894
    }
1✔
895

896
    #[test]
897
    fn test_validate_document_invalid_response_json() {
1✔
898
        let mut doc = create_test_document();
1✔
899
        doc.sections.push(Section {
1✔
900
            section_type: SectionType::Request,
1✔
901
            content: SectionContent::Json(serde_json::json!({"key": "value"})),
1✔
902
            inline_options: InlineOptions::default(),
1✔
903
            raw_content: "{\"key\": \"value\"}".to_string(),
1✔
904
            start_line: 5,
1✔
905
            end_line: 6,
1✔
906
        });
1✔
907
        doc.sections.push(Section {
1✔
908
            section_type: SectionType::Response,
1✔
909
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
910
            inline_options: InlineOptions::default(),
1✔
911
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
912
            start_line: 7,
1✔
913
            end_line: 8,
1✔
914
        });
1✔
915

916
        let errors = validate_document_diagnostics(&doc);
1✔
917
        // Valid JSON should have no errors
918
        let has_json_errors = errors.iter().any(|e| e.message.contains("JSON"));
1✔
919
        assert!(!has_json_errors);
1✔
920
    }
1✔
921

922
    #[test]
923
    fn test_validate_error_details_must_be_array() {
1✔
924
        let mut doc = create_test_document();
1✔
925
        doc.sections.push(Section {
1✔
926
            section_type: SectionType::Error,
1✔
927
            content: SectionContent::Json(serde_json::json!({
1✔
928
                "code": 3,
1✔
929
                "details": {"@type": "type.googleapis.com/google.rpc.ErrorInfo"}
1✔
930
            })),
1✔
931
            inline_options: InlineOptions::default(),
1✔
932
            raw_content: "".to_string(),
1✔
933
            start_line: 5,
1✔
934
            end_line: 8,
1✔
935
        });
1✔
936

937
        let errors = validate_document_diagnostics(&doc);
1✔
938
        assert!(
1✔
939
            errors
1✔
940
                .iter()
1✔
941
                .any(|e| e.message.contains("field 'details' must be an array"))
1✔
942
        );
943
    }
1✔
944

945
    #[test]
946
    fn test_validate_error_details_items_must_be_objects() {
1✔
947
        let mut doc = create_test_document();
1✔
948
        doc.sections.push(Section {
1✔
949
            section_type: SectionType::Error,
1✔
950
            content: SectionContent::Json(serde_json::json!({
1✔
951
                "code": 3,
1✔
952
                "details": ["not-an-object"]
1✔
953
            })),
1✔
954
            inline_options: InlineOptions::default(),
1✔
955
            raw_content: "".to_string(),
1✔
956
            start_line: 5,
1✔
957
            end_line: 8,
1✔
958
        });
1✔
959

960
        let errors = validate_document_diagnostics(&doc);
1✔
961
        assert!(
1✔
962
            errors
1✔
963
                .iter()
1✔
964
                .any(|e| e.message.contains("'details' items must be objects"))
1✔
965
        );
966
    }
1✔
967

968
    #[test]
969
    fn test_validate_document_address_from_env() {
1✔
970
        // Set env var
971
        unsafe {
1✔
972
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_ADDRESS, "env:5000");
1✔
973
        }
1✔
974

975
        let mut doc = GctfDocument::new("test.gctf".to_string());
1✔
976
        doc.sections.push(Section {
1✔
977
            section_type: SectionType::Endpoint,
1✔
978
            content: SectionContent::Single("Service/Method".to_string()),
1✔
979
            inline_options: InlineOptions::default(),
1✔
980
            raw_content: "Service/Method".to_string(),
1✔
981
            start_line: 1,
1✔
982
            end_line: 1,
1✔
983
        });
1✔
984
        doc.sections.push(Section {
1✔
985
            section_type: SectionType::Response,
1✔
986
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
987
            inline_options: InlineOptions::default(),
1✔
988
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
989
            start_line: 2,
1✔
990
            end_line: 3,
1✔
991
        });
1✔
992

993
        let result = validate_document(&doc);
1✔
994
        // Should pass because address comes from env
995
        assert!(result.is_ok());
1✔
996

997
        // Clean up
998
        unsafe {
1✔
999
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_ADDRESS);
1✔
1000
        }
1✔
1001
    }
1✔
1002

1003
    #[test]
1004
    fn test_validate_options_unknown_key_warning() {
1✔
1005
        let mut doc = create_test_document();
1✔
1006
        let mut options = std::collections::HashMap::new();
1✔
1007
        options.insert("unknown".to_string(), "value".to_string());
1✔
1008
        doc.sections.push(Section {
1✔
1009
            section_type: SectionType::Options,
1✔
1010
            content: SectionContent::KeyValues(options),
1✔
1011
            inline_options: InlineOptions::default(),
1✔
1012
            raw_content: "unknown: value".to_string(),
1✔
1013
            start_line: 5,
1✔
1014
            end_line: 6,
1✔
1015
        });
1✔
1016
        doc.sections.push(Section {
1✔
1017
            section_type: SectionType::Response,
1✔
1018
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
1019
            inline_options: InlineOptions::default(),
1✔
1020
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
1021
            start_line: 7,
1✔
1022
            end_line: 8,
1✔
1023
        });
1✔
1024

1025
        let diagnostics = validate_document_diagnostics(&doc);
1✔
1026
        assert!(diagnostics.iter().any(|d| {
1✔
1027
            d.severity == ErrorSeverity::Warning && d.message.contains("Unknown OPTIONS key")
1✔
1028
        }));
1✔
1029
    }
1✔
1030

1031
    #[test]
1032
    fn test_validate_options_dry_run_is_unknown_key_warning() {
1✔
1033
        let mut doc = create_test_document();
1✔
1034
        let mut options = std::collections::HashMap::new();
1✔
1035
        options.insert("dry_run".to_string(), "true".to_string());
1✔
1036
        doc.sections.push(Section {
1✔
1037
            section_type: SectionType::Options,
1✔
1038
            content: SectionContent::KeyValues(options),
1✔
1039
            inline_options: InlineOptions::default(),
1✔
1040
            raw_content: "dry_run: true".to_string(),
1✔
1041
            start_line: 5,
1✔
1042
            end_line: 6,
1✔
1043
        });
1✔
1044
        doc.sections.push(Section {
1✔
1045
            section_type: SectionType::Response,
1✔
1046
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
1047
            inline_options: InlineOptions::default(),
1✔
1048
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
1049
            start_line: 7,
1✔
1050
            end_line: 8,
1✔
1051
        });
1✔
1052

1053
        let diagnostics = validate_document_diagnostics(&doc);
1✔
1054
        assert!(diagnostics.iter().any(|d| {
1✔
1055
            d.severity == ErrorSeverity::Warning
1✔
1056
                && d.message
1✔
1057
                    .contains("Unknown OPTIONS key 'dry_run'. Supported keys: timeout, retry, retry-delay, no-retry, compression")
1✔
1058
        }));
1✔
1059
    }
1✔
1060

1061
    #[test]
1062
    fn test_validate_options_timeout_invalid_error() {
1✔
1063
        let mut doc = create_test_document();
1✔
1064
        let mut options = std::collections::HashMap::new();
1✔
1065
        options.insert("timeout".to_string(), "0".to_string());
1✔
1066
        doc.sections.push(Section {
1✔
1067
            section_type: SectionType::Options,
1✔
1068
            content: SectionContent::KeyValues(options),
1✔
1069
            inline_options: InlineOptions::default(),
1✔
1070
            raw_content: "timeout: 0".to_string(),
1✔
1071
            start_line: 5,
1✔
1072
            end_line: 6,
1✔
1073
        });
1✔
1074
        doc.sections.push(Section {
1✔
1075
            section_type: SectionType::Response,
1✔
1076
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
1077
            inline_options: InlineOptions::default(),
1✔
1078
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
1079
            start_line: 7,
1✔
1080
            end_line: 8,
1✔
1081
        });
1✔
1082

1083
        let diagnostics = validate_document_diagnostics(&doc);
1✔
1084
        assert!(diagnostics.iter().any(|d| {
1✔
1085
            d.severity == ErrorSeverity::Error
1✔
1086
                && d.message
1✔
1087
                    .contains("OPTIONS.timeout must be a positive integer")
1✔
1088
        }));
1✔
1089
    }
1✔
1090

1091
    #[test]
1092
    fn test_validate_options_kebab_case_keys_are_supported() {
1✔
1093
        let mut doc = create_test_document();
1✔
1094
        let mut options = std::collections::HashMap::new();
1✔
1095
        options.insert("timeout".to_string(), "5".to_string());
1✔
1096
        options.insert("retry".to_string(), "2".to_string());
1✔
1097
        options.insert("retry-delay".to_string(), "0.5".to_string());
1✔
1098
        options.insert("no-retry".to_string(), "false".to_string());
1✔
1099
        options.insert("compression".to_string(), "gzip".to_string());
1✔
1100
        doc.sections.push(Section {
1✔
1101
            section_type: SectionType::Options,
1✔
1102
            content: SectionContent::KeyValues(options),
1✔
1103
            inline_options: InlineOptions::default(),
1✔
1104
            raw_content: "".to_string(),
1✔
1105
            start_line: 5,
1✔
1106
            end_line: 8,
1✔
1107
        });
1✔
1108
        doc.sections.push(Section {
1✔
1109
            section_type: SectionType::Response,
1✔
1110
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
1111
            inline_options: InlineOptions::default(),
1✔
1112
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
1113
            start_line: 9,
1✔
1114
            end_line: 10,
1✔
1115
        });
1✔
1116

1117
        let diagnostics = validate_document_diagnostics(&doc);
1✔
1118
        assert!(
1✔
1119
            !diagnostics
1✔
1120
                .iter()
1✔
1121
                .any(|d| d.message.contains("Unknown OPTIONS key"))
1✔
1122
        );
1123
        assert!(
1✔
1124
            !diagnostics
1✔
1125
                .iter()
1✔
1126
                .any(|d| d.severity == ErrorSeverity::Error)
1✔
1127
        );
1128
    }
1✔
1129

1130
    #[test]
1131
    fn test_validate_options_compression_invalid_error() {
1✔
1132
        let mut doc = create_test_document();
1✔
1133
        let mut options = std::collections::HashMap::new();
1✔
1134
        options.insert("compression".to_string(), "brotli".to_string());
1✔
1135
        doc.sections.push(Section {
1✔
1136
            section_type: SectionType::Options,
1✔
1137
            content: SectionContent::KeyValues(options),
1✔
1138
            inline_options: InlineOptions::default(),
1✔
1139
            raw_content: "compression: brotli".to_string(),
1✔
1140
            start_line: 5,
1✔
1141
            end_line: 6,
1✔
1142
        });
1✔
1143
        doc.sections.push(Section {
1✔
1144
            section_type: SectionType::Response,
1✔
1145
            content: SectionContent::Json(serde_json::json!({"result": "ok"})),
1✔
1146
            inline_options: InlineOptions::default(),
1✔
1147
            raw_content: "{\"result\": \"ok\"}".to_string(),
1✔
1148
            start_line: 7,
1✔
1149
            end_line: 8,
1✔
1150
        });
1✔
1151

1152
        let diagnostics = validate_document_diagnostics(&doc);
1✔
1153
        assert!(diagnostics.iter().any(|d| {
1✔
1154
            d.severity == ErrorSeverity::Error
1✔
1155
                && d.message
1✔
1156
                    .contains("OPTIONS.compression must be one of: none, gzip")
1✔
1157
        }));
1✔
1158
    }
1✔
1159

1160
    #[test]
1161
    fn test_validation_error_debug() {
1✔
1162
        let error = ValidationError {
1✔
1163
            message: "test error".to_string(),
1✔
1164
            line: Some(10),
1✔
1165
            severity: ErrorSeverity::Error,
1✔
1166
        };
1✔
1167
        let debug_str = format!("{:?}", error);
1✔
1168
        assert!(debug_str.contains("ValidationError"));
1✔
1169
        assert!(debug_str.contains("test error"));
1✔
1170
    }
1✔
1171

1172
    #[test]
1173
    fn test_error_severity_serialize() {
1✔
1174
        let error = ErrorSeverity::Error;
1✔
1175
        let json = serde_json::to_string(&error).unwrap();
1✔
1176
        assert_eq!(json, "\"error\"");
1✔
1177

1178
        let warning = ErrorSeverity::Warning;
1✔
1179
        let json = serde_json::to_string(&warning).unwrap();
1✔
1180
        assert_eq!(json, "\"warning\"");
1✔
1181
    }
1✔
1182
}
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