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

gripmock / grpctestify-rust / 24902954483

24 Apr 2026 05:29PM UTC coverage: 78.02% (+0.3%) from 77.729%
24902954483

Pull #43

github

web-flow
Merge b18cb9c15 into 017e47d15
Pull Request #43: new command gen grpcurl & call

741 of 993 new or added lines in 24 files covered. (74.62%)

3 existing lines in 3 files now uncovered.

19594 of 25114 relevant lines covered (78.02%)

39580.43 hits per line

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

46.02
/src/execution/runner.rs
1
// Test runner
2
// Executes tests defined in GctfDocument
3
// Refactored to use RequestHandler, ResponseHandler, AssertionHandler
4

5
use super::super::parser::GctfDocument;
6
use super::runner_helpers;
7
use super::{AssertionHandler, RequestHandler, ResponseHandler};
8
use crate::assert::{AssertionEngine, JsonComparator, get_json_diff};
9
use crate::grpc::{GrpcClient, GrpcClientConfig};
10
use crate::optimizer;
11
use crate::parser::ast::{SectionContent, SectionType};
12
use crate::plugins::AssertionTiming;
13
use crate::report::CoverageCollector;
14
use anyhow::Result;
15
use serde::{Deserialize, Serialize};
16
use serde_json::Value;
17
use std::collections::HashMap;
18
use std::path::Path;
19
use std::sync::Arc;
20
use tokio::sync::mpsc;
21
use tokio_stream::StreamExt;
22
use tokio_stream::wrappers::ReceiverStream;
23

24
/// Execution plan for inspect workflow visualization
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct ExecutionPlan {
27
    pub file_path: String,
28
    pub connection: ConnectionInfo,
29
    pub target: TargetInfo,
30
    pub headers: Option<HeadersInfo>,
31
    pub requests: Vec<RequestInfo>,
32
    pub expectations: Vec<ExpectationInfo>,
33
    pub assertions: Vec<AssertionInfo>,
34
    pub extractions: Vec<ExtractionInfo>,
35
    pub rpc_mode: RpcMode,
36
    pub summary: ExecutionSummary,
37
}
38

39
#[derive(Debug, Clone, Serialize, Deserialize)]
40
pub struct ConnectionInfo {
41
    pub address: String,
42
    pub source: String,
43
}
44

45
#[derive(Debug, Clone, Serialize, Deserialize)]
46
pub struct TargetInfo {
47
    pub endpoint: String,
48
    pub package: Option<String>,
49
    pub service: Option<String>,
50
    pub method: Option<String>,
51
}
52

53
#[derive(Debug, Clone, Serialize, Deserialize)]
54
pub struct HeadersInfo {
55
    pub count: usize,
56
    pub headers: HashMap<String, String>,
57
}
58

59
#[derive(Debug, Clone, Serialize, Deserialize)]
60
pub struct RequestInfo {
61
    pub index: usize,
62
    pub content: Value,
63
    pub content_type: String,
64
    pub line_start: usize,
65
    pub line_end: usize,
66
}
67

68
#[derive(Debug, Clone, Serialize, Deserialize)]
69
pub struct ExpectationInfo {
70
    pub index: usize,
71
    pub expectation_type: String, // "response" or "error"
72
    pub content: Option<Value>,
73
    pub message_count: Option<usize>,
74
    pub comparison_options: ComparisonOptions,
75
    pub line_start: usize,
76
    pub line_end: usize,
77
}
78

79
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80
pub struct ComparisonOptions {
81
    pub partial: bool,
82
    pub redact: Vec<String>,
83
    pub tolerance: Option<f64>,
84
    pub unordered_arrays: bool,
85
    pub with_asserts: bool,
86
}
87

88
#[derive(Debug, Clone, Serialize, Deserialize)]
89
pub struct AssertionInfo {
90
    pub index: usize,
91
    pub assertions: Vec<String>,
92
    pub line_start: usize,
93
    pub line_end: usize,
94
}
95

96
#[derive(Debug, Clone, Serialize, Deserialize)]
97
pub struct ExtractionInfo {
98
    pub index: usize,
99
    pub variables: HashMap<String, String>,
100
    pub line_start: usize,
101
    pub line_end: usize,
102
}
103

104
#[derive(Debug, Clone, Serialize, Deserialize)]
105
#[serde(rename_all = "snake_case")]
106
pub enum RpcMode {
107
    Unary,
108
    UnaryError,
109
    ServerStreaming {
110
        response_count: usize,
111
    },
112
    ClientStreaming {
113
        request_count: usize,
114
    },
115
    BidirectionalStreaming {
116
        request_count: usize,
117
        response_count: usize,
118
    },
119
    Unknown,
120
}
121

122
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123
pub struct ExecutionSummary {
124
    pub total_requests: usize,
125
    pub total_responses: usize,
126
    pub total_errors: usize,
127
    pub error_expected: bool,
128
    pub assertion_blocks: usize,
129
    pub variable_extractions: usize,
130
    pub rpc_mode_name: String,
131
}
132

133
struct AssertionContext<'a> {
134
    headers: &'a HashMap<String, String>,
135
    trailers: &'a HashMap<String, String>,
136
    timing: Option<&'a AssertionTiming>,
137
}
138

139
impl ExecutionPlan {
140
    /// Build execution plan from a GctfDocument
141
    pub fn from_document(doc: &GctfDocument) -> Self {
77✔
142
        let file_path = doc.file_path.clone();
77✔
143

144
        // Connection info
145
        let connection = if let Some(section) = doc.first_section(SectionType::Address) {
77✔
146
            if let SectionContent::Single(addr) = &section.content {
4✔
147
                ConnectionInfo {
4✔
148
                    address: addr.clone(),
4✔
149
                    source: format!(
4✔
150
                        "ADDRESS section [Line {}-{}]",
4✔
151
                        section.start_line, section.end_line
4✔
152
                    ),
4✔
153
                }
4✔
154
            } else {
155
                ConnectionInfo {
×
156
                    address: "<env:GRPCTESTIFY_ADDRESS>".to_string(),
×
157
                    source: "Environment variable (implicit)".to_string(),
×
158
                }
×
159
            }
160
        } else {
161
            ConnectionInfo {
73✔
162
                address: "<env:GRPCTESTIFY_ADDRESS>".to_string(),
73✔
163
                source: "Environment variable (implicit)".to_string(),
73✔
164
            }
73✔
165
        };
166

167
        // Target info
168
        let target = if let Some(section) = doc.first_section(SectionType::Endpoint) {
77✔
169
            if let SectionContent::Single(endpoint) = &section.content {
77✔
170
                let (package, service, method) = doc
77✔
171
                    .parse_endpoint()
77✔
172
                    .map(|(p, s, m)| (Some(p), Some(s), Some(m)))
77✔
173
                    .unwrap_or((None, None, None));
77✔
174
                TargetInfo {
77✔
175
                    endpoint: endpoint.clone(),
77✔
176
                    package,
77✔
177
                    service,
77✔
178
                    method,
77✔
179
                }
77✔
180
            } else {
181
                TargetInfo {
×
182
                    endpoint: "<missing>".to_string(),
×
183
                    package: None,
×
184
                    service: None,
×
185
                    method: None,
×
186
                }
×
187
            }
188
        } else {
189
            TargetInfo {
×
190
                endpoint: "<missing>".to_string(),
×
191
                package: None,
×
192
                service: None,
×
193
                method: None,
×
194
            }
×
195
        };
196

197
        // Headers info
198
        let headers = doc
77✔
199
            .first_section(SectionType::RequestHeaders)
77✔
200
            .and_then(|section| {
77✔
201
                if let SectionContent::KeyValues(headers) = &section.content {
4✔
202
                    Some(HeadersInfo {
4✔
203
                        count: headers.len(),
4✔
204
                        headers: headers.clone(),
4✔
205
                    })
4✔
206
                } else {
207
                    None
×
208
                }
209
            });
4✔
210

211
        // Requests
212
        let request_sections = doc.sections_by_type(SectionType::Request);
77✔
213
        let requests: Vec<RequestInfo> = request_sections
77✔
214
            .iter()
77✔
215
            .enumerate()
77✔
216
            .map(|(i, section)| {
86✔
217
                let (content, content_type) = match &section.content {
86✔
218
                    SectionContent::Json(j) => (j.clone(), "json"),
86✔
219
                    SectionContent::JsonLines(_) => (Value::Array(vec![]), "json-lines"),
×
220
                    SectionContent::Empty => (Value::Object(serde_json::Map::new()), "empty"),
×
221
                    _ => (Value::Null, "unknown"),
×
222
                };
223
                RequestInfo {
86✔
224
                    index: i + 1,
86✔
225
                    content,
86✔
226
                    content_type: content_type.to_string(),
86✔
227
                    line_start: section.start_line,
86✔
228
                    line_end: section.end_line,
86✔
229
                }
86✔
230
            })
86✔
231
            .collect();
77✔
232

233
        // Expectations (responses or error)
234
        let response_sections = doc.sections_by_type(SectionType::Response);
77✔
235
        let error_section = doc.first_section(SectionType::Error);
77✔
236

237
        let expectations: Vec<ExpectationInfo> = if !response_sections.is_empty() {
77✔
238
            response_sections
71✔
239
                .iter()
71✔
240
                .enumerate()
71✔
241
                .map(|(i, section)| {
81✔
242
                    let (content, message_count) = match &section.content {
81✔
243
                        SectionContent::Json(j) => (Some(j.clone()), None),
81✔
244
                        SectionContent::JsonLines(vals) => (None, Some(vals.len())),
×
245
                        _ => (None, None),
×
246
                    };
247
                    ExpectationInfo {
81✔
248
                        index: i + 1,
81✔
249
                        expectation_type: "response".to_string(),
81✔
250
                        content,
81✔
251
                        message_count,
81✔
252
                        comparison_options: ComparisonOptions {
81✔
253
                            partial: section.inline_options.partial,
81✔
254
                            redact: section.inline_options.redact.clone(),
81✔
255
                            tolerance: section.inline_options.tolerance,
81✔
256
                            unordered_arrays: section.inline_options.unordered_arrays,
81✔
257
                            with_asserts: section.inline_options.with_asserts,
81✔
258
                        },
81✔
259
                        line_start: section.start_line,
81✔
260
                        line_end: section.end_line,
81✔
261
                    }
81✔
262
                })
81✔
263
                .collect()
71✔
264
        } else if let Some(section) = error_section {
6✔
265
            let content = match &section.content {
5✔
266
                SectionContent::Json(j) => Some(j.clone()),
5✔
267
                _ => None,
×
268
            };
269
            vec![ExpectationInfo {
5✔
270
                index: 1,
5✔
271
                expectation_type: "error".to_string(),
5✔
272
                content,
5✔
273
                message_count: None,
5✔
274
                comparison_options: ComparisonOptions {
5✔
275
                    partial: section.inline_options.partial,
5✔
276
                    redact: section.inline_options.redact.clone(),
5✔
277
                    tolerance: section.inline_options.tolerance,
5✔
278
                    unordered_arrays: section.inline_options.unordered_arrays,
5✔
279
                    with_asserts: section.inline_options.with_asserts,
5✔
280
                },
5✔
281
                line_start: section.start_line,
5✔
282
                line_end: section.end_line,
5✔
283
            }]
5✔
284
        } else {
285
            vec![]
1✔
286
        };
287

288
        // Assertions
289
        let assert_sections = doc.sections_by_type(SectionType::Asserts);
77✔
290
        let assertions: Vec<AssertionInfo> = assert_sections
77✔
291
            .iter()
77✔
292
            .enumerate()
77✔
293
            .map(|(i, section)| {
77✔
294
                let assertions = if let SectionContent::Assertions(lines) = &section.content {
30✔
295
                    lines
30✔
296
                        .iter()
30✔
297
                        .map(|line| {
51✔
298
                            optimizer::rewrite_assertion_expression_fixed_point_if_changed(line)
51✔
299
                                .unwrap_or_else(|| line.clone())
51✔
300
                        })
51✔
301
                        .collect()
30✔
302
                } else {
303
                    vec![]
×
304
                };
305
                AssertionInfo {
30✔
306
                    index: i + 1,
30✔
307
                    assertions,
30✔
308
                    line_start: section.start_line,
30✔
309
                    line_end: section.end_line,
30✔
310
                }
30✔
311
            })
30✔
312
            .collect();
77✔
313

314
        // Extractions
315
        let extract_sections = doc.sections_by_type(SectionType::Extract);
77✔
316
        let extractions: Vec<ExtractionInfo> = extract_sections
77✔
317
            .iter()
77✔
318
            .enumerate()
77✔
319
            .map(|(i, section)| {
77✔
320
                let variables = if let SectionContent::Extract(vars) = &section.content {
21✔
321
                    vars.clone()
21✔
322
                } else {
323
                    HashMap::new()
×
324
                };
325
                ExtractionInfo {
21✔
326
                    index: i + 1,
21✔
327
                    variables,
21✔
328
                    line_start: section.start_line,
21✔
329
                    line_end: section.end_line,
21✔
330
                }
21✔
331
            })
21✔
332
            .collect();
77✔
333

334
        // Infer RPC mode
335
        let has_json_lines = response_sections
77✔
336
            .iter()
77✔
337
            .any(|s| matches!(&s.content, SectionContent::JsonLines(vals) if vals.len() > 1));
77✔
338
        let rpc_mode = infer_rpc_mode(
77✔
339
            &requests,
77✔
340
            &expectations,
77✔
341
            error_section.is_some(),
77✔
342
            has_json_lines,
77✔
343
        );
344

345
        // Summary
346
        let rpc_mode_name = match &rpc_mode {
77✔
347
            RpcMode::Unary => "Unary",
61✔
348
            RpcMode::UnaryError => "Unary Error",
5✔
349
            RpcMode::ServerStreaming { .. } => "Server Streaming",
5✔
350
            RpcMode::ClientStreaming { .. } => "Client Streaming",
5✔
351
            RpcMode::BidirectionalStreaming { .. } => "Bidirectional Streaming",
×
352
            RpcMode::Unknown => "Unknown",
1✔
353
        };
354

355
        let summary = ExecutionSummary {
77✔
356
            total_requests: requests.len(),
77✔
357
            total_responses: expectations
77✔
358
                .iter()
77✔
359
                .filter(|e| e.expectation_type == "response")
86✔
360
                .count(),
77✔
361
            total_errors: expectations
77✔
362
                .iter()
77✔
363
                .filter(|e| e.expectation_type == "error")
86✔
364
                .count(),
77✔
365
            error_expected: expectations.iter().any(|e| e.expectation_type == "error"),
86✔
366
            assertion_blocks: assertions.len(),
77✔
367
            variable_extractions: extractions.len(),
77✔
368
            rpc_mode_name: rpc_mode_name.to_string(),
77✔
369
        };
370

371
        ExecutionPlan {
77✔
372
            file_path,
77✔
373
            connection,
77✔
374
            target,
77✔
375
            headers,
77✔
376
            requests,
77✔
377
            expectations,
77✔
378
            assertions,
77✔
379
            extractions,
77✔
380
            rpc_mode,
77✔
381
            summary,
77✔
382
        }
77✔
383
    }
77✔
384
}
385

386
fn infer_rpc_mode(
77✔
387
    requests: &[RequestInfo],
77✔
388
    expectations: &[ExpectationInfo],
77✔
389
    has_error: bool,
77✔
390
    has_json_lines: bool,
77✔
391
) -> RpcMode {
77✔
392
    let req_count = requests.len();
77✔
393
    let resp_count = expectations
77✔
394
        .iter()
77✔
395
        .filter(|e| e.expectation_type == "response")
86✔
396
        .count();
77✔
397

398
    if has_error {
77✔
399
        RpcMode::UnaryError
5✔
400
    } else if has_json_lines || resp_count > 1 {
72✔
401
        if req_count > 1 {
5✔
402
            RpcMode::BidirectionalStreaming {
×
403
                request_count: req_count,
×
404
                response_count: resp_count,
×
405
            }
×
406
        } else {
407
            RpcMode::ServerStreaming {
5✔
408
                response_count: resp_count,
5✔
409
            }
5✔
410
        }
411
    } else if req_count > 1 {
67✔
412
        RpcMode::ClientStreaming {
5✔
413
            request_count: req_count,
5✔
414
        }
5✔
415
    } else if req_count == 1 && resp_count == 1 {
62✔
416
        RpcMode::Unary
61✔
417
    } else if req_count == 0 && resp_count > 0 {
1✔
418
        RpcMode::ServerStreaming {
×
419
            response_count: resp_count,
×
420
        }
×
421
    } else {
422
        RpcMode::Unknown
1✔
423
    }
424
}
77✔
425

426
/// Test execution status
427
#[derive(Debug, Clone, PartialEq, Eq)]
428
pub enum TestExecutionStatus {
429
    Pass,
430
    Fail(String),
431
}
432

433
/// Test execution result
434
#[derive(Debug, Clone, PartialEq, Eq)]
435
pub struct TestExecutionResult {
436
    pub status: TestExecutionStatus,
437
    pub grpc_duration_ms: Option<u64>,
438
    // Optional: captured response for updating the test file
439
    pub captured_response: Option<crate::grpc::GrpcResponse>,
440
}
441

442
#[derive(Debug, Default, Clone)]
443
struct AssertionScopeTimingState {
444
    last_message_elapsed_ms: Option<u64>,
445
    total_scope_elapsed_ms: u64,
446
    scope_index: usize,
447
}
448

449
impl AssertionScopeTimingState {
450
    fn finish_scope(
4✔
451
        &mut self,
4✔
452
        scope_start_ms: u64,
4✔
453
        scope_end_ms: u64,
4✔
454
        scope_message_count: usize,
4✔
455
    ) -> Option<AssertionTiming> {
4✔
456
        if scope_message_count == 0 {
4✔
457
            return None;
×
458
        }
4✔
459

460
        let elapsed_ms = scope_end_ms.saturating_sub(scope_start_ms);
4✔
461
        self.scope_index += 1;
4✔
462
        self.total_scope_elapsed_ms = self.total_scope_elapsed_ms.saturating_add(elapsed_ms);
4✔
463

464
        let timing = AssertionTiming {
4✔
465
            elapsed_ms,
4✔
466
            total_elapsed_ms: self.total_scope_elapsed_ms,
4✔
467
            scope_message_count,
4✔
468
            scope_index: self.scope_index,
4✔
469
        };
4✔
470

471
        Some(timing)
4✔
472
    }
4✔
473
}
474

475
impl TestExecutionResult {
476
    pub fn pass(grpc_duration_ms: Option<u64>) -> Self {
10✔
477
        Self {
10✔
478
            status: TestExecutionStatus::Pass,
10✔
479
            grpc_duration_ms,
10✔
480
            captured_response: None,
10✔
481
        }
10✔
482
    }
10✔
483

484
    pub fn fail(message: String, grpc_duration_ms: Option<u64>) -> Self {
7✔
485
        Self {
7✔
486
            status: TestExecutionStatus::Fail(message),
7✔
487
            grpc_duration_ms,
7✔
488
            captured_response: None,
7✔
489
        }
7✔
490
    }
7✔
491

492
    pub fn with_response(mut self, response: crate::grpc::GrpcResponse) -> Self {
×
493
        self.captured_response = Some(response);
×
494
        self
×
495
    }
×
496
}
497

498
/// Test runner
499
pub struct TestRunner {
500
    dry_run: bool,
501
    timeout_seconds: u64,
502
    no_assert: bool,
503
    write_mode: bool,
504
    verbose: bool,
505
    assertion_engine: AssertionEngine,
506
    coverage_collector: Option<Arc<CoverageCollector>>,
507
    // Handler modules for delegated functionality
508
    request_handler: RequestHandler,
509
    response_handler: ResponseHandler,
510
    assertion_handler: AssertionHandler,
511
}
512

513
impl TestRunner {
514
    /// Create expected values from a response section.
515
    pub fn expected_values_for_response_section(
3✔
516
        section: &crate::parser::ast::Section,
3✔
517
    ) -> Vec<Value> {
3✔
518
        match &section.content {
3✔
519
            SectionContent::Json(v) => vec![v.clone()],
2✔
520
            SectionContent::JsonLines(values) => values.clone(),
1✔
521
            _ => Vec::new(),
×
522
        }
523
    }
3✔
524

525
    pub fn grpc_code_name_from_numeric(code: i64) -> Option<&'static str> {
4✔
526
        super::error_handler::ErrorHandler::grpc_code_name_from_numeric(code)
4✔
527
    }
4✔
528

529
    pub fn error_matches_expected(error_text: &str, expected: &Value) -> bool {
5✔
530
        super::error_handler::ErrorHandler::error_matches_expected(error_text, expected)
5✔
531
    }
5✔
532

533
    fn has_required_followup_asserts(
2✔
534
        section: &crate::parser::ast::Section,
2✔
535
        sections: &[crate::parser::ast::Section],
2✔
536
        index: usize,
2✔
537
        effective_no_assert: bool,
2✔
538
        failure_reasons: &mut Vec<String>,
2✔
539
    ) -> bool {
2✔
540
        if !section.inline_options.with_asserts {
2✔
541
            return false;
×
542
        }
2✔
543

544
        if sections
2✔
545
            .get(index + 1)
2✔
546
            .is_some_and(|next| next.section_type == SectionType::Asserts)
2✔
547
        {
548
            return true;
1✔
549
        }
1✔
550

551
        if !effective_no_assert {
1✔
552
            failure_reasons.push(format!(
1✔
553
                "{} at line {} has 'with_asserts' but is not followed by ASSERTS",
1✔
554
                section.section_type.as_str(),
1✔
555
                section.start_line
1✔
556
            ));
1✔
557
        }
1✔
558

559
        false
1✔
560
    }
2✔
561

562
    /// Create a new test runner
563
    pub fn new(
23✔
564
        dry_run: bool,
23✔
565
        timeout_seconds: u64,
23✔
566
        no_assert: bool,
23✔
567
        write_mode: bool,
23✔
568
        verbose: bool,
23✔
569
        coverage_collector: Option<Arc<CoverageCollector>>,
23✔
570
    ) -> Self {
23✔
571
        Self {
23✔
572
            dry_run,
23✔
573
            timeout_seconds,
23✔
574
            no_assert,
23✔
575
            write_mode,
23✔
576
            verbose,
23✔
577
            assertion_engine: AssertionEngine::new(),
23✔
578
            coverage_collector: coverage_collector.clone(),
23✔
579
            // Initialize handler modules
23✔
580
            request_handler: RequestHandler::new(no_assert, verbose, coverage_collector.clone()),
23✔
581
            response_handler: ResponseHandler::new(no_assert),
23✔
582
            assertion_handler: AssertionHandler::new(verbose),
23✔
583
        }
23✔
584
    }
23✔
585

586
    /// Run a test document chain.
587
    /// Walks the `next_document` linked list, accumulating EXTRACT variables
588
    /// between documents. Fail-fast: stops on first failure.
589
    pub async fn run_test(&self, document: &GctfDocument) -> Result<TestExecutionResult> {
1✔
590
        let mut variables: HashMap<String, Value> = HashMap::new();
1✔
591
        let mut overall_status = TestExecutionStatus::Pass;
1✔
592
        let mut total_duration_ms = 0.0;
1✔
593

594
        for doc in document.iter_chain() {
1✔
595
            let result = self.run_one(doc, &mut variables).await?;
1✔
596
            if let Some(dur) = result.grpc_duration_ms {
1✔
597
                total_duration_ms += dur as f64;
×
598
            }
1✔
599

600
            if let TestExecutionStatus::Fail(msg) = &result.status {
1✔
601
                overall_status = TestExecutionStatus::Fail(msg.clone());
×
602
                break;
×
603
            }
1✔
604
        }
605

606
        // Build summary result
607
        Ok(TestExecutionResult {
1✔
608
            status: overall_status,
1✔
609
            grpc_duration_ms: Some(total_duration_ms as u64),
1✔
610
            captured_response: None,
1✔
611
        })
1✔
612
    }
1✔
613

614
    /// Run a single document, sharing variables with the chain.
615
    async fn run_one(
1✔
616
        &self,
1✔
617
        document: &GctfDocument,
1✔
618
        variables: &mut HashMap<String, Value>,
1✔
619
    ) -> Result<TestExecutionResult> {
1✔
620
        let effective_dry_run = self.dry_run;
1✔
621
        let effective_no_assert = self.no_assert;
1✔
622
        let effective_write_mode = self.write_mode;
1✔
623

624
        let options = document.get_options().unwrap_or_default();
1✔
625
        let effective_timeout_seconds = match options.get("timeout") {
1✔
626
            Some(value) => match value.trim().parse::<u64>() {
×
627
                Ok(v) if v > 0 => v,
×
628
                _ => {
629
                    return Ok(TestExecutionResult::fail(
×
630
                        format!(
×
631
                            "OPTIONS.timeout must be a positive integer, got '{}'",
×
632
                            value
×
633
                        ),
×
634
                        None,
×
635
                    ));
×
636
                }
637
            },
638
            None => self.timeout_seconds,
1✔
639
        };
640

641
        let compression = runner_helpers::parse_compression_option(&options);
1✔
642

643
        // Validate file path in update mode
644
        if effective_write_mode {
1✔
645
            let file_path = Path::new(&document.file_path);
×
646
            if !file_path.exists() {
×
647
                return Ok(TestExecutionResult::fail(
×
648
                    format!("Update mode: file '{}' does not exist", document.file_path),
×
649
                    None,
×
650
                ));
×
651
            }
×
652

653
            // Check if file is writable
654
            use std::fs::OpenOptions;
655
            if OpenOptions::new().write(true).open(file_path).is_err() {
×
656
                return Ok(TestExecutionResult::fail(
×
657
                    format!("Update mode: file '{}' is not writable", document.file_path),
×
658
                    None,
×
659
                ));
×
660
            }
×
661
        }
1✔
662

663
        // Extract address
664
        let address = runner_helpers::effective_address(document);
1✔
665

666
        // Extract endpoint
667
        let (package, service, method) = match document.parse_endpoint() {
1✔
668
            Some(e) => e,
1✔
669
            None => {
670
                return Ok(TestExecutionResult::fail(
×
671
                    "Invalid or missing endpoint".to_string(),
×
672
                    None,
×
673
                ));
×
674
            }
675
        };
676

677
        if document.sections.is_empty() {
1✔
678
            return Ok(TestExecutionResult::fail(
×
679
                "No sections found".to_string(),
×
680
                None,
×
681
            ));
×
682
        }
1✔
683

684
        if effective_dry_run {
1✔
685
            // In dry-run, show detailed preview of what will be executed
686
            self.print_dry_run_preview(document, &address, &package, &service, &method);
1✔
687
            return Ok(TestExecutionResult::pass(None));
1✔
688
        }
×
689

690
        // Configure Client
691
        let document_path = Path::new(&document.file_path);
×
692

693
        let tls_config = runner_helpers::build_tls_config(document, document_path);
×
694

695
        // Check for Proto config in document
696
        let proto_config = runner_helpers::build_proto_config(document, document_path);
×
697

698
        let full_service = runner_helpers::full_service_name(&package, &service);
×
699

700
        let client_config = GrpcClientConfig {
×
701
            address,
×
702
            timeout_seconds: effective_timeout_seconds,
×
703
            tls_config,
×
704
            proto_config,
×
705
            metadata: document.get_request_headers(),
×
706
            target_service: Some(full_service.clone()),
×
707
            compression,
×
708
        };
×
709

710
        let client = GrpcClient::new(client_config).await?;
×
711

712
        // Get input/output message types for field coverage tracking
713
        let input_message_type = client
×
714
            .descriptor_pool()
×
715
            .get_service_by_name(&full_service)
×
716
            .and_then(|s| s.methods().find(|m| m.name() == method))
×
717
            .map(|m| m.input().full_name().to_string());
×
718
        let output_message_type = client
×
719
            .descriptor_pool()
×
720
            .get_service_by_name(&full_service)
×
721
            .and_then(|s| s.methods().find(|m| m.name() == method))
×
722
            .map(|m| m.output().full_name().to_string());
×
723

724
        // Setup Streaming
NEW
725
        let (tx, rx) = mpsc::channel::<Value>(runner_helpers::REQUEST_CHANNEL_BUFFER);
×
726
        let request_stream = ReceiverStream::new(rx);
×
727
        let mut tx = Some(tx);
×
728

729
        // Coverage: Register pool and record call
730
        if let Some(collector) = &self.coverage_collector {
×
731
            collector.register_pool(client.descriptor_pool());
×
732
            collector.record_call(&full_service, &method);
×
733
        }
×
734

735
        let start_time = std::time::Instant::now();
×
736

737
        // Start the gRPC call in background so unary/server-streaming methods can wait
738
        // for the first request message without deadlocking this task.
739
        let full_service_clone = full_service.clone();
×
740
        let method_clone = method.clone();
×
741
        let mut client_for_call = client;
×
742
        let mut call_handle = Some(tokio::spawn(async move {
×
743
            client_for_call
×
744
                .call_stream(&full_service_clone, &method_clone, request_stream)
×
745
                .await
×
746
        }));
×
747

748
        let mut response_stream = None;
×
749

750
        // variables passed from caller (shared across chain)
751
        let mut last_message: Option<Value> = None;
×
752
        let mut last_error_message: Option<String> = None;
×
753
        let mut last_error_json: Option<Value> = None;
×
754
        let mut last_error_timing: Option<AssertionTiming> = None;
×
755
        let mut captured_headers: HashMap<String, String> = HashMap::new();
×
756
        let mut captured_trailers: HashMap<String, String> = HashMap::new();
×
757
        let mut failure_reasons: Vec<String> = Vec::new();
×
758
        let mut assertion_timing = AssertionScopeTimingState::default();
×
759

760
        // Iterator for sections
761
        // We iterate by index to allow lookahead
762
        let sections = &document.sections;
×
763

764
        // Pre-compute last request index to avoid O(n²) lookups in loop
765
        let last_request_idx = sections
×
766
            .iter()
×
767
            .rposition(|s| s.section_type == SectionType::Request);
×
768

769
        let has_request_sections = sections
×
770
            .iter()
×
771
            .any(|s| s.section_type == SectionType::Request);
×
772

773
        // Legacy behavior: if no REQUEST section is provided, send an empty
774
        // JSON object as a single request message for unary/server-stream calls.
775
        if !has_request_sections && let Some(tx_ref) = tx.as_mut() {
×
776
            if let Err(e) = tx_ref.send(Value::Object(serde_json::Map::new())).await {
×
777
                failure_reasons.push(format!("Failed to send implicit empty request: {}", e));
×
778
            }
×
779
            drop(tx.take());
×
780
        }
×
781

782
        let mut skip_next_section = false;
×
783

784
        // Capture full response for write mode
785
        let mut captured_response = if effective_write_mode {
×
786
            Some(crate::grpc::GrpcResponse::new())
×
787
        } else {
788
            None
×
789
        };
790

791
        /// Awaits the gRPC call handle and extracts the response stream.
792
        /// Eliminates duplication across Response and Asserts section handlers.
793
        macro_rules! ensure_stream_ready {
794
            () => {
795
                if response_stream.is_none()
796
                    && let Some(handle) = call_handle.take()
797
                {
798
                    match handle.await {
799
                        Ok(Ok((h, stream))) => {
800
                            if let Some(resp) = &mut captured_response {
801
                                captured_headers = h.clone();
802
                                resp.headers = h;
803
                            } else {
804
                                captured_headers = h;
805
                            }
806
                            response_stream = Some(stream);
807
                        }
808
                        Ok(Err(e)) => {
809
                            failure_reasons.push(format!("Failed to start gRPC stream: {}", e));
810
                            break;
811
                        }
812
                        Err(e) => {
813
                            failure_reasons
814
                                .push(format!("Failed to join gRPC stream startup task: {}", e));
815
                            break;
816
                        }
817
                    }
818
                }
819
            };
820
        }
821

822
        for (i, section) in sections.iter().enumerate() {
×
823
            if skip_next_section {
×
824
                skip_next_section = false;
×
825
                continue;
×
826
            }
×
827

828
            match section.section_type {
×
829
                SectionType::Request => {
830
                    // Build request using RequestHandler
831
                    let request_value = match &section.content {
×
832
                        SectionContent::Json(req_json) => {
×
833
                            let mut req = req_json.clone();
×
834
                            self.substitute_variables(&mut req, variables);
×
835
                            req
×
836
                        }
837
                        SectionContent::Empty => Value::Object(serde_json::Map::new()),
×
838
                        _ => continue,
×
839
                    };
840

841
                    // Coverage: record request fields
842
                    if let (Some(collector), Some(msg_type)) =
×
843
                        (&self.coverage_collector, &input_message_type)
×
844
                    {
×
845
                        collector.record_fields_from_json(msg_type, &request_value);
×
846
                    }
×
847

848
                    // Send request using RequestHandler
849
                    let Some(tx_ref) = tx.as_mut() else {
×
850
                        failure_reasons.push(format!(
×
851
                            "Failed to send request at line {}: request stream already closed",
852
                            section.start_line
853
                        ));
854
                        break;
×
855
                    };
856

857
                    let result = self
×
858
                        .request_handler
×
859
                        .send_request(tx_ref, request_value, section.start_line, None)
×
860
                        .await;
×
861
                    if !result.success
×
862
                        && let Some(error) = result.error_message
×
863
                    {
×
864
                        failure_reasons.push(error);
×
865
                    }
×
866
                }
867
                SectionType::Response => {
868
                    let scope_start_ms = assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
869
                    let mut scope_end_ms = scope_start_ms;
×
870
                    let mut scope_message_count = 0usize;
×
871

872
                    if i >= last_request_idx.unwrap_or(usize::MAX) {
×
873
                        drop(tx.take());
×
874
                    }
×
875

876
                    ensure_stream_ready!();
×
877

878
                    let mut received_messages_for_section: Vec<Value> = Vec::new();
×
879
                    let expected_values = Self::expected_values_for_response_section(section);
×
880

NEW
881
                    let Some(stream) = response_stream.as_mut() else {
×
NEW
882
                        failure_reasons.push(format!(
×
883
                            "No response stream available for RESPONSE section at line {}",
884
                            section.start_line
885
                        ));
NEW
886
                        break;
×
887
                    };
888

889
                    for expected_template in expected_values {
×
NEW
890
                        match stream.next().await {
×
891
                            Some(Ok(item)) => {
×
892
                                match item {
×
893
                                    crate::grpc::client::StreamItem::Message(msg) => {
×
894
                                        let now_elapsed_ms =
×
895
                                            start_time.elapsed().as_millis() as u64;
×
896

897
                                        let msg_for_state = msg.clone();
×
898
                                        last_message = Some(msg_for_state.clone());
×
899
                                        if section.inline_options.with_asserts {
×
900
                                            received_messages_for_section
×
901
                                                .push(msg_for_state.clone());
×
902
                                        }
×
903
                                        scope_end_ms = now_elapsed_ms;
×
904
                                        scope_message_count += 1;
×
905
                                        assertion_timing.last_message_elapsed_ms =
×
906
                                            Some(now_elapsed_ms);
×
907
                                        if let Some(resp) = &mut captured_response {
×
908
                                            resp.messages.push(msg_for_state);
×
909
                                        }
×
910

911
                                        Self::log_response_message(
×
912
                                            &msg,
×
913
                                            effective_no_assert,
×
914
                                            self.verbose,
×
915
                                        );
916

917
                                        if !effective_no_assert {
×
918
                                            let mut expected = expected_template.clone();
×
919
                                            self.substitute_variables(&mut expected, variables);
×
920

921
                                            // Coverage: record expected response fields
922
                                            if let (Some(collector), Some(msg_type)) =
×
923
                                                (&self.coverage_collector, &output_message_type)
×
924
                                            {
×
925
                                                collector
×
926
                                                    .record_fields_from_json(msg_type, &expected);
×
927
                                            }
×
928

929
                                            let diffs = JsonComparator::compare(
×
930
                                                &msg,
×
931
                                                &expected,
×
932
                                                &section.inline_options,
×
933
                                            );
934

935
                                            if !diffs.is_empty() {
×
936
                                                self.append_response_diffs(
×
937
                                                    diffs,
×
938
                                                    section.start_line,
×
939
                                                    &expected,
×
940
                                                    &msg,
×
941
                                                    &mut failure_reasons,
×
942
                                                );
×
943
                                            }
×
944
                                        }
×
945
                                    }
946
                                    crate::grpc::client::StreamItem::Trailers(t) => {
×
947
                                        if let Some(resp) = &mut captured_response {
×
948
                                            captured_trailers.extend(
×
949
                                                t.iter().map(|(k, v)| (k.clone(), v.clone())),
×
950
                                            );
951
                                            resp.trailers.extend(t);
×
952
                                        } else {
×
953
                                            captured_trailers.extend(t);
×
954
                                        }
×
955
                                        if !effective_no_assert {
×
956
                                            failure_reasons.push(format!(
×
957
                                                "Expected message for RESPONSE section at line {}, but received Trailers (End of Stream)",
×
958
                                                section.start_line
×
959
                                            ));
×
960
                                        }
×
961
                                        break;
×
962
                                    }
963
                                }
964
                            }
965
                            Some(Err(status)) => {
×
966
                                let scope_start_ms =
×
967
                                    assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
968
                                let scope_end_ms = start_time.elapsed().as_millis() as u64;
×
969
                                assertion_timing.last_message_elapsed_ms = Some(scope_end_ms);
×
970
                                last_error_timing =
×
971
                                    assertion_timing.finish_scope(scope_start_ms, scope_end_ms, 1);
×
972
                                last_error_message = Some(status.message().to_string());
×
973

974
                                if let Some(resp) = &mut captured_response {
×
975
                                    resp.error = Some(status.message().to_string());
×
976
                                }
×
977
                                if !effective_no_assert {
×
978
                                    failure_reasons.push(format!(
×
979
                                        "Expected message for RESPONSE section at line {}, but received Error: {}",
×
980
                                        section.start_line,
×
981
                                        status.message()
×
982
                                    ));
×
983
                                } else {
×
984
                                    println!("--- RESPONSE (Error) ---");
×
985
                                    println!("{}", status.message());
×
986
                                }
×
987
                                break;
×
988
                            }
989
                            None => {
990
                                if !effective_no_assert {
×
991
                                    failure_reasons.push(format!(
×
992
                                        "Expected message for RESPONSE section at line {}, but stream ended",
×
993
                                        section.start_line
×
994
                                    ));
×
995
                                }
×
996
                                break;
×
997
                            }
998
                        }
999
                    }
1000

1001
                    if section.inline_options.with_asserts
×
1002
                        && let Some(next_section) = sections.get(i + 1)
×
1003
                        && next_section.section_type == SectionType::Asserts
×
1004
                    {
1005
                        if !effective_no_assert
×
1006
                            && let SectionContent::Assertions(lines) = &next_section.content
×
1007
                        {
1008
                            let scope_timing = assertion_timing.finish_scope(
×
1009
                                scope_start_ms,
×
1010
                                scope_end_ms,
×
1011
                                scope_message_count,
×
1012
                            );
1013

1014
                            for msg in &received_messages_for_section {
×
1015
                                self.run_assertions(
×
1016
                                    lines,
×
1017
                                    msg,
×
1018
                                    &mut failure_reasons,
×
1019
                                    format!(
×
1020
                                        "(attached to RESPONSE at line {})",
×
1021
                                        section.start_line
×
1022
                                    ),
×
1023
                                    section.start_line,
×
1024
                                    AssertionContext {
×
1025
                                        headers: &captured_headers,
×
1026
                                        trailers: &captured_trailers,
×
1027
                                        timing: scope_timing.as_ref(),
×
1028
                                    },
×
1029
                                );
×
1030
                            }
×
1031
                        }
×
1032
                        skip_next_section = true;
×
1033
                    } else if section.inline_options.with_asserts && !effective_no_assert {
×
1034
                        failure_reasons.push(format!(
×
1035
                            "RESPONSE at line {} has 'with_asserts' but is not followed by ASSERTS",
×
1036
                            section.start_line
×
1037
                        ));
×
1038
                    }
×
1039
                }
1040
                SectionType::Asserts => {
1041
                    if i >= last_request_idx.unwrap_or(usize::MAX) {
×
1042
                        drop(tx.take());
×
1043
                    }
×
1044

1045
                    ensure_stream_ready!();
×
1046

1047
                    // Standalone ASSERTS usually consumes a new message.
1048
                    // If stream is unavailable but we already captured an ERROR,
1049
                    // evaluate assertions against that error context.
1050
                    let Some(stream) = response_stream.as_mut() else {
×
1051
                        if !effective_no_assert
×
1052
                            && let SectionContent::Assertions(lines) = &section.content
×
1053
                        {
1054
                            if let Some(error_value) = &last_error_json {
×
1055
                                self.run_assertions(
×
1056
                                    lines,
×
1057
                                    error_value,
×
1058
                                    &mut failure_reasons,
×
1059
                                    format!("after ERROR at line {}", section.start_line),
×
1060
                                    section.start_line,
×
1061
                                    AssertionContext {
×
1062
                                        headers: &captured_headers,
×
1063
                                        trailers: &captured_trailers,
×
1064
                                        timing: last_error_timing.as_ref(),
×
1065
                                    },
×
1066
                                );
×
1067
                            } else if let Some(error_message) = &last_error_message {
×
1068
                                let error_value = Value::String(error_message.clone());
×
1069
                                self.run_assertions(
×
1070
                                    lines,
×
1071
                                    &error_value,
×
1072
                                    &mut failure_reasons,
×
1073
                                    format!("after ERROR at line {}", section.start_line),
×
1074
                                    section.start_line,
×
1075
                                    AssertionContext {
×
1076
                                        headers: &captured_headers,
×
1077
                                        trailers: &captured_trailers,
×
1078
                                        timing: last_error_timing.as_ref(),
×
1079
                                    },
×
1080
                                );
×
1081
                            } else {
×
1082
                                failure_reasons.push(format!(
×
1083
                                    "ASSERTS section at line {} has no active response/error context",
×
1084
                                    section.start_line
×
1085
                                ));
×
1086
                            }
×
1087
                        }
×
1088
                        continue;
×
1089
                    };
1090

1091
                    match stream.next().await {
×
1092
                        Some(Ok(crate::grpc::client::StreamItem::Message(msg))) => {
×
1093
                            let scope_start_ms =
×
1094
                                assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
1095
                            let scope_end_ms = start_time.elapsed().as_millis() as u64;
×
1096
                            assertion_timing.last_message_elapsed_ms = Some(scope_end_ms);
×
1097
                            let scope_timing =
×
1098
                                assertion_timing.finish_scope(scope_start_ms, scope_end_ms, 1);
×
1099

1100
                            last_message = Some(msg.clone());
×
1101

1102
                            let should_format_message =
×
1103
                                tracing::enabled!(tracing::Level::DEBUG) || effective_no_assert;
×
1104
                            let msg_pretty = should_format_message
×
1105
                                .then(|| runner_helpers::format_json_pretty(&msg));
×
1106

1107
                            if let Some(pretty) = msg_pretty.as_deref()
×
1108
                                && tracing::enabled!(tracing::Level::DEBUG)
×
1109
                            {
1110
                                tracing::debug!("Received Response (for Asserts):\n{}", pretty);
×
1111
                            }
×
1112

1113
                            if effective_no_assert {
×
1114
                                println!("--- RESPONSE (Raw) ---");
×
1115
                                if let Some(pretty) = msg_pretty.as_deref() {
×
1116
                                    println!("{}", pretty);
×
1117
                                }
×
1118
                            }
×
1119

1120
                            if !effective_no_assert
×
1121
                                && let SectionContent::Assertions(lines) = &section.content
×
1122
                            {
×
1123
                                self.run_assertions(
×
1124
                                    lines,
×
1125
                                    &msg,
×
1126
                                    &mut failure_reasons,
×
1127
                                    format!("at line {}", section.start_line),
×
1128
                                    section.start_line,
×
1129
                                    AssertionContext {
×
1130
                                        headers: &captured_headers,
×
1131
                                        trailers: &captured_trailers,
×
1132
                                        timing: scope_timing.as_ref(),
×
1133
                                    },
×
1134
                                );
×
1135
                            }
×
1136
                        }
1137
                        Some(Ok(crate::grpc::client::StreamItem::Trailers(t))) => {
×
1138
                            captured_trailers.extend(t);
×
1139
                            if !effective_no_assert {
×
1140
                                failure_reasons.push(format!(
×
1141
                                    "Expected message for ASSERTS section at line {}, but received Trailers",
×
1142
                                    section.start_line
×
1143
                                ));
×
1144
                            }
×
1145
                        }
1146
                        Some(Err(status)) => {
×
1147
                            let scope_start_ms =
×
1148
                                assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
1149
                            let scope_end_ms = start_time.elapsed().as_millis() as u64;
×
1150
                            assertion_timing.last_message_elapsed_ms = Some(scope_end_ms);
×
1151
                            last_error_timing =
×
1152
                                assertion_timing.finish_scope(scope_start_ms, scope_end_ms, 1);
×
1153

1154
                            last_error_message = Some(status.message().to_string());
×
1155
                            let error_json =
×
1156
                                super::error_handler::ErrorHandler::status_to_json(&status);
×
1157
                            last_error_json = Some(error_json.clone());
×
1158
                            captured_trailers
×
1159
                                .extend(runner_helpers::metadata_map_to_hashmap(status.metadata()));
×
1160
                            if !effective_no_assert
×
1161
                                && let SectionContent::Assertions(lines) = &section.content
×
1162
                            {
×
1163
                                self.run_assertions(
×
1164
                                    lines,
×
1165
                                    &error_json,
×
1166
                                    &mut failure_reasons,
×
1167
                                    format!("after ERROR at line {}", section.start_line),
×
1168
                                    section.start_line,
×
1169
                                    AssertionContext {
×
1170
                                        headers: &captured_headers,
×
1171
                                        trailers: &captured_trailers,
×
1172
                                        timing: last_error_timing.as_ref(),
×
1173
                                    },
×
1174
                                );
×
1175
                            } else {
×
1176
                                println!("--- RESPONSE (Error) ---");
×
1177
                                println!("{}", status.message());
×
1178
                            }
×
1179
                        }
1180
                        None => {
1181
                            if !effective_no_assert {
×
1182
                                failure_reasons.push(format!(
×
1183
                                    "Expected message for ASSERTS section at line {}, but stream ended",
×
1184
                                    section.start_line
×
1185
                                ));
×
1186
                            }
×
1187
                        }
1188
                    }
1189
                }
1190

1191
                SectionType::Extract => {
1192
                    if let Some(msg) = &last_message {
×
1193
                        if let SectionContent::Extract(extractions) = &section.content {
×
1194
                            for (key, query) in extractions {
×
1195
                                match self.assertion_engine.query(query, msg) {
×
1196
                                    Ok(results) => {
×
1197
                                        if let Some(val) = results.first() {
×
1198
                                            variables.insert(key.clone(), val.clone());
×
1199
                                        } else {
×
1200
                                            failure_reasons.push(format!(
×
1201
                                                 "Extraction failed at line {}: Query '{}' returned no results",
×
1202
                                                 section.start_line, query
×
1203
                                             ));
×
1204
                                        }
×
1205
                                    }
1206
                                    Err(e) => {
×
1207
                                        failure_reasons.push(format!(
×
1208
                                            "Extraction error at line {}: {}",
×
1209
                                            section.start_line, e
×
1210
                                        ));
×
1211
                                    }
×
1212
                                }
1213
                            }
1214
                        }
×
1215
                    } else {
×
1216
                        failure_reasons.push(format!(
×
1217
                            "EXTRACT at line {} requires a previous response message",
×
1218
                            section.start_line
×
1219
                        ));
×
1220
                    }
×
1221
                }
1222
                SectionType::Error => {
1223
                    if i >= last_request_idx.unwrap_or(usize::MAX) {
×
1224
                        drop(tx.take());
×
1225
                    }
×
1226

1227
                    if response_stream.is_none()
×
1228
                        && let Some(handle) = call_handle.take()
×
1229
                    {
1230
                        match handle.await {
×
1231
                            Ok(Ok((h, stream))) => {
×
1232
                                if let Some(resp) = &mut captured_response {
×
1233
                                    captured_headers = h.clone();
×
1234
                                    resp.headers = h;
×
1235
                                } else {
×
1236
                                    captured_headers = h;
×
1237
                                }
×
1238
                                response_stream = Some(stream);
×
1239
                            }
1240
                            Ok(Err(_e)) => {
×
1241
                                let e = _e;
×
1242
                                let mut error_assert_target: Option<Value> = None;
×
1243
                                if !effective_no_assert {
×
1244
                                    if let SectionContent::Json(expected_json) = &section.content {
×
1245
                                        let mut expected = expected_json.clone();
×
1246
                                        self.substitute_variables(&mut expected, variables);
×
1247

1248
                                        // Try to extract tonic Status from anyhow::Error
1249
                                        let (matches, got, mismatch_reason) = if let Some(status) =
×
1250
                                            e.downcast_ref::<tonic::Status>()
×
1251
                                        {
1252
                                            last_error_message = Some(status.message().to_string());
×
1253
                                            let actual_error_json =
×
1254
                                                super::error_handler::ErrorHandler::status_to_json(
×
1255
                                                    status,
×
1256
                                                );
1257
                                            last_error_json = Some(actual_error_json.clone());
×
1258
                                            error_assert_target = Some(actual_error_json.clone());
×
1259
                                            captured_trailers.extend(
×
1260
                                                runner_helpers::metadata_map_to_hashmap(
×
1261
                                                    status.metadata(),
×
1262
                                                ),
1263
                                            );
1264
                                            let status_name = Self::grpc_code_name_from_numeric(
×
1265
                                                status.code() as i64,
×
1266
                                            )
1267
                                            .unwrap_or("Unknown");
×
1268
                                            (
×
1269
                                                super::error_handler::ErrorHandler::status_matches_expected_with_options(
×
1270
                                                    status,
×
1271
                                                    &expected,
×
1272
                                                    section.inline_options.partial,
×
1273
                                                ),
×
1274
                                                format!(
×
1275
                                                    "status: {}, message: \"{}\"",
×
1276
                                                    status_name,
×
1277
                                                    status.message()
×
1278
                                                ),
×
1279
                                                super::error_handler::ErrorHandler::status_mismatch_reason_with_options(
×
1280
                                                    status,
×
1281
                                                    &expected,
×
1282
                                                    section.inline_options.partial,
×
1283
                                                ),
×
1284
                                            )
×
1285
                                        } else {
1286
                                            // Fallback to error string representation
1287
                                            let text = e.to_string();
×
1288
                                            error_assert_target = Some(Value::String(text.clone()));
×
1289
                                            (
×
1290
                                                Self::error_matches_expected(&text, &expected),
×
1291
                                                text,
×
1292
                                                None,
×
1293
                                            )
×
1294
                                        };
1295

1296
                                        if self.verbose {
×
1297
                                            println!("🔍 gRPC error received: '{}'", got);
×
1298
                                            if let Some(status) = e.downcast_ref::<tonic::Status>()
×
1299
                                            {
1300
                                                let details_json = super::error_handler::ErrorHandler::status_details_json(status);
×
1301
                                                if details_json != Value::Null
×
1302
                                                    && details_json
×
1303
                                                        .as_array()
×
1304
                                                        .is_some_and(|arr| !arr.is_empty())
×
1305
                                                {
×
1306
                                                    println!(
×
1307
                                                        "🔍 gRPC error details: {}",
×
1308
                                                        details_json
×
1309
                                                    );
×
1310
                                                }
×
1311
                                            }
×
1312
                                        }
×
1313

1314
                                        if !matches {
×
1315
                                            failure_reasons.push(format!(
×
1316
                                                "Error mismatch at line {}:",
1317
                                                section.start_line
1318
                                            ));
1319
                                            if let Some(reason) = mismatch_reason {
×
1320
                                                failure_reasons.push(format!("  - {}", reason));
×
1321
                                            }
×
1322
                                            if let Some(status) = e.downcast_ref::<tonic::Status>()
×
1323
                                            {
×
1324
                                                let actual_json =
×
1325
                                                    super::error_handler::ErrorHandler::status_to_json(
×
1326
                                                        status,
×
1327
                                                    );
×
1328
                                                failure_reasons
×
1329
                                                    .push(get_json_diff(&expected, &actual_json));
×
1330
                                            } else {
×
1331
                                                failure_reasons.push(format!(
×
1332
                                                    "  - expected {}, got '{}'",
×
1333
                                                    expected, got
×
1334
                                                ));
×
1335
                                            }
×
1336
                                        }
×
1337
                                    }
×
1338
                                } else {
×
1339
                                    println!("--- RESPONSE (Error) ---");
×
1340
                                    println!("{}", e);
×
1341
                                }
×
1342

1343
                                if Self::has_required_followup_asserts(
×
1344
                                    section,
×
1345
                                    sections,
×
1346
                                    i,
×
1347
                                    effective_no_assert,
×
1348
                                    &mut failure_reasons,
×
1349
                                ) && let Some(next_section) = sections.get(i + 1)
×
1350
                                    && next_section.section_type == SectionType::Asserts
×
1351
                                {
1352
                                    if !effective_no_assert
×
1353
                                        && let SectionContent::Assertions(lines) =
×
1354
                                            &next_section.content
×
1355
                                    {
1356
                                        if error_assert_target.is_none()
×
1357
                                            && let Some(status) = e.downcast_ref::<tonic::Status>()
×
1358
                                        {
×
1359
                                            let actual_error_json =
×
1360
                                                super::error_handler::ErrorHandler::status_to_json(
×
1361
                                                    status,
×
1362
                                                );
×
1363
                                            last_error_json = Some(actual_error_json.clone());
×
1364
                                            error_assert_target = Some(actual_error_json);
×
1365
                                        }
×
1366

1367
                                        if let Some(target) = error_assert_target.as_ref() {
×
1368
                                            self.run_assertions(
×
1369
                                                lines,
×
1370
                                                target,
×
1371
                                                &mut failure_reasons,
×
1372
                                                format!(
×
1373
                                                    "(attached to ERROR at line {})",
×
1374
                                                    section.start_line
×
1375
                                                ),
×
1376
                                                section.start_line,
×
1377
                                                AssertionContext {
×
1378
                                                    headers: &captured_headers,
×
1379
                                                    trailers: &captured_trailers,
×
1380
                                                    timing: last_error_timing.as_ref(),
×
1381
                                                },
×
1382
                                            );
×
1383
                                        }
×
1384
                                    }
×
1385
                                    skip_next_section = true;
×
1386
                                }
×
1387

1388
                                // Error has been consumed at startup stage; continue with next sections.
1389
                                continue;
×
1390
                            }
1391
                            Err(e) => {
×
1392
                                failure_reasons.push(format!(
×
1393
                                    "Failed to join gRPC stream startup task: {}",
1394
                                    e
1395
                                ));
1396
                                break;
×
1397
                            }
1398
                        }
1399
                    }
×
1400

1401
                    // Expect an error from the stream
NEW
1402
                    let Some(error_stream) = response_stream.as_mut() else {
×
NEW
1403
                        failure_reasons.push(format!(
×
1404
                            "No response stream available for ERROR section at line {}",
1405
                            section.start_line
1406
                        ));
NEW
1407
                        break;
×
1408
                    };
NEW
1409
                    match error_stream.next().await {
×
1410
                        Some(Err(status)) => {
×
1411
                            let scope_start_ms =
×
1412
                                assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
1413
                            let scope_end_ms = start_time.elapsed().as_millis() as u64;
×
1414
                            assertion_timing.last_message_elapsed_ms = Some(scope_end_ms);
×
1415
                            let error_scope_timing =
×
1416
                                assertion_timing.finish_scope(scope_start_ms, scope_end_ms, 1);
×
1417

1418
                            let status_message = status.message();
×
1419
                            last_error_message = Some(status_message.to_string());
×
1420
                            let error_json =
×
1421
                                super::error_handler::ErrorHandler::status_to_json(&status);
×
1422
                            last_error_json = Some(error_json.clone());
×
1423
                            last_error_timing = error_scope_timing;
×
1424
                            captured_trailers
×
1425
                                .extend(runner_helpers::metadata_map_to_hashmap(status.metadata()));
×
1426
                            let should_format_error = effective_no_assert || self.verbose;
×
1427
                            let got = should_format_error.then(|| {
×
1428
                                let status_name =
×
1429
                                    Self::grpc_code_name_from_numeric(status.code() as i64)
×
1430
                                        .unwrap_or("Unknown");
×
1431
                                format!("status: {}, message: \"{}\"", status_name, status_message)
×
1432
                            });
×
1433

1434
                            if effective_no_assert {
×
1435
                                println!("--- RESPONSE (Error) ---");
×
1436
                                if let Some(got) = got.as_deref() {
×
1437
                                    println!("{}", got);
×
1438
                                }
×
1439
                            } else if self.verbose {
×
1440
                                if let Some(got) = got.as_deref() {
×
1441
                                    println!("🔍 gRPC error received: '{}'", got);
×
1442
                                }
×
1443
                                let details_json =
×
1444
                                    super::error_handler::ErrorHandler::status_details_json(
×
1445
                                        &status,
×
1446
                                    );
1447
                                if details_json != Value::Null
×
1448
                                    && details_json.as_array().is_some_and(|arr| !arr.is_empty())
×
1449
                                {
×
1450
                                    println!("🔍 gRPC error details: {}", details_json);
×
1451
                                }
×
1452
                            }
×
1453

1454
                            if !effective_no_assert {
×
1455
                                if let SectionContent::Json(expected_json) = &section.content {
×
1456
                                    let mut expected = expected_json.clone();
×
1457
                                    self.substitute_variables(&mut expected, variables);
×
1458

1459
                                    if !super::error_handler::ErrorHandler::status_matches_expected_with_options(
×
1460
                                        &status,
×
1461
                                        &expected,
×
1462
                                        section.inline_options.partial,
×
1463
                                    ) {
×
1464
                                        failure_reasons.push(format!(
×
1465
                                            "Error mismatch at line {}:",
1466
                                            section.start_line
1467
                                        ));
1468
                                        if let Some(reason) =
×
1469
                                            super::error_handler::ErrorHandler::status_mismatch_reason_with_options(
×
1470
                                                &status,
×
1471
                                                &expected,
×
1472
                                                section.inline_options.partial,
×
1473
                                            )
×
1474
                                        {
×
1475
                                            failure_reasons.push(format!("  - {}", reason));
×
1476
                                        }
×
1477
                                        let actual_json =
×
1478
                                            super::error_handler::ErrorHandler::status_to_json(
×
1479
                                                &status,
×
1480
                                            );
1481
                                        failure_reasons
×
1482
                                            .push(get_json_diff(&expected, &actual_json));
×
1483
                                    }
×
1484
                                }
×
1485

1486
                                // Handle with_asserts for Error
1487
                                if Self::has_required_followup_asserts(
×
1488
                                    section,
×
1489
                                    sections,
×
1490
                                    i,
×
1491
                                    effective_no_assert,
×
1492
                                    &mut failure_reasons,
×
1493
                                ) && let Some(next_section) = sections.get(i + 1)
×
1494
                                    && next_section.section_type == SectionType::Asserts
×
1495
                                    && let SectionContent::Assertions(lines) = &next_section.content
×
1496
                                {
×
1497
                                    self.run_assertions(
×
1498
                                        lines,
×
1499
                                        &error_json,
×
1500
                                        &mut failure_reasons,
×
1501
                                        format!(
×
1502
                                            "(attached to ERROR at line {})",
×
1503
                                            section.start_line
×
1504
                                        ),
×
1505
                                        section.start_line,
×
1506
                                        AssertionContext {
×
1507
                                            headers: &captured_headers,
×
1508
                                            trailers: &captured_trailers,
×
1509
                                            timing: last_error_timing.as_ref(),
×
1510
                                        },
×
1511
                                    );
×
1512
                                    skip_next_section = true;
×
1513
                                }
×
1514
                            } else {
1515
                                // In no_assert mode, we still need to skip the attached ASSERTS section if present
1516
                                if section.inline_options.with_asserts
×
1517
                                    && let Some(next_section) = sections.get(i + 1)
×
1518
                                    && next_section.section_type == SectionType::Asserts
×
1519
                                {
×
1520
                                    skip_next_section = true;
×
1521
                                }
×
1522
                            }
1523
                        }
1524
                        Some(Ok(msg_item)) => {
×
1525
                            if !effective_no_assert {
×
1526
                                failure_reasons.push(format!(
×
1527
                                    "Expected ERROR at line {}, but received success message or trailers",
×
1528
                                    section.start_line
×
1529
                                ));
×
1530
                            } else {
×
1531
                                // If we got a message instead of error in no_assert mode, print it
1532
                                if let crate::grpc::client::StreamItem::Message(msg) = msg_item {
×
1533
                                    println!("--- RESPONSE (Raw) ---");
×
1534
                                    println!("{}", runner_helpers::format_json_pretty(&msg));
×
1535
                                }
×
1536
                            }
1537
                        }
1538
                        None => {
1539
                            if !effective_no_assert {
×
1540
                                failure_reasons.push(format!(
×
1541
                                    "Expected ERROR at line {}, but stream ended successfully",
×
1542
                                    section.start_line
×
1543
                                ));
×
1544
                            }
×
1545
                        }
1546
                    }
1547
                }
1548
                _ => {}
×
1549
            }
1550
        }
1551

1552
        // Ensure we close the request stream
1553
        drop(tx.take());
×
1554

1555
        // If in update mode, capture any remaining responses
1556
        if let Some(resp) = &mut captured_response {
×
1557
            if response_stream.is_none()
×
1558
                && let Some(handle) = call_handle.take()
×
1559
            {
1560
                match handle.await {
×
1561
                    Ok(Ok((h, stream))) => {
×
1562
                        resp.headers = h;
×
1563
                        response_stream = Some(stream);
×
1564
                    }
×
1565
                    Ok(Err(_)) | Err(_) => {
×
1566
                        response_stream = None;
×
1567
                    }
×
1568
                }
1569
            }
×
1570

1571
            loop {
1572
                let next_item = if let Some(stream) = response_stream.as_mut() {
×
1573
                    stream.next().await
×
1574
                } else {
1575
                    None
×
1576
                };
1577

1578
                let Some(item_res) = next_item else {
×
1579
                    break;
×
1580
                };
1581

1582
                match item_res {
×
1583
                    Ok(crate::grpc::client::StreamItem::Message(msg)) => {
×
1584
                        resp.messages.push(msg);
×
1585
                    }
×
1586
                    Ok(crate::grpc::client::StreamItem::Trailers(t)) => {
×
1587
                        resp.trailers.extend(t);
×
1588
                    }
×
1589
                    Err(status) => {
×
1590
                        resp.error = Some(status.message().to_string());
×
1591
                    }
×
1592
                }
1593
            }
1594
        }
×
1595

1596
        let grpc_duration = start_time.elapsed().as_millis() as u64;
×
1597

1598
        if !failure_reasons.is_empty() {
×
1599
            // Even if failed, we might want to return captured response?
1600
            // Usually snapshot update only happens if user asks for it.
1601
            // If write_mode is true, we should probably ignore failures?
1602
            if effective_write_mode {
×
1603
                // In write mode, failures (mismatches) are expected because we are updating!
1604
                // But validation errors (like invalid JSON) might still be relevant.
1605
                // Let's assume update mode implies "I want to overwrite whatever happens".
1606
                if let Some(resp) = captured_response {
×
1607
                    return Ok(TestExecutionResult::pass(Some(grpc_duration)).with_response(resp));
×
1608
                }
×
1609
            }
×
1610

1611
            return Ok(TestExecutionResult::fail(
×
1612
                format!("Validation failed:\n  - {}", failure_reasons.join("\n  - ")),
×
1613
                Some(grpc_duration),
×
1614
            ));
×
1615
        }
×
1616

1617
        let mut result = TestExecutionResult::pass(Some(grpc_duration));
×
1618
        if let Some(resp) = captured_response {
×
1619
            result = result.with_response(resp);
×
1620
        }
×
1621
        Ok(result)
×
1622
    }
1✔
1623

1624
    /// Validates a collected response against the document (for testing purposes)
1625
    pub fn validate_response(
12✔
1626
        &self,
12✔
1627
        document: &GctfDocument,
12✔
1628
        response: &crate::grpc::GrpcResponse,
12✔
1629
    ) -> TestExecutionResult {
12✔
1630
        self.response_handler.validate_document(document, response)
12✔
1631
    }
12✔
1632

1633
    fn substitute_variables(&self, value: &mut Value, variables: &HashMap<String, Value>) {
3✔
1634
        match value {
3✔
1635
            Value::String(s) => {
3✔
1636
                if !s.contains("{{") {
3✔
1637
                    return;
×
1638
                }
3✔
1639

1640
                // Check for exact match "{{ var }}" to preserve type
1641
                if s.starts_with("{{") && s.ends_with("}}") {
3✔
1642
                    let inner = s[2..s.len() - 2].trim();
1✔
1643
                    // check if inner has more {{ }} which implies complex string
1644
                    if !inner.contains("{{")
1✔
1645
                        && let Some(val) = variables.get(inner)
1✔
1646
                    {
1647
                        *value = val.clone();
1✔
1648
                        return;
1✔
1649
                    }
×
1650
                }
2✔
1651

1652
                // String interpolation "prefix {{ var }} suffix"
1653
                if let Some(result) = runner_helpers::interpolate_variables(s, variables) {
2✔
1654
                    *value = Value::String(result);
2✔
1655
                }
2✔
1656
            }
1657
            Value::Array(arr) => {
×
1658
                for v in arr {
×
1659
                    self.substitute_variables(v, variables);
×
1660
                }
×
1661
            }
1662
            Value::Object(obj) => {
×
1663
                for v in obj.values_mut() {
×
1664
                    self.substitute_variables(v, variables);
×
1665
                }
×
1666
            }
1667
            _ => {}
×
1668
        }
1669
    }
3✔
1670

1671
    fn run_assertions(
1✔
1672
        &self,
1✔
1673
        lines: &[String],
1✔
1674
        target_value: &Value,
1✔
1675
        failure_reasons: &mut Vec<String>,
1✔
1676
        context: String,
1✔
1677
        start_line: usize,
1✔
1678
        assertion_context: AssertionContext<'_>,
1✔
1679
    ) {
1✔
1680
        let mut optimized_lines: Option<Vec<String>> = None;
1✔
1681

1682
        for (idx, line) in lines.iter().enumerate() {
3✔
1683
            if let Some(rewritten) =
×
1684
                optimizer::rewrite_assertion_expression_fixed_point_if_changed(line)
3✔
1685
            {
1686
                let vec = optimized_lines.get_or_insert_with(|| lines[..idx].to_vec());
×
1687
                vec.push(rewritten);
×
1688
            } else if let Some(vec) = optimized_lines.as_mut() {
3✔
1689
                vec.push(line.clone());
×
1690
            }
3✔
1691
        }
1692

1693
        let lines_to_evaluate: &[String] = optimized_lines.as_deref().unwrap_or(lines);
1✔
1694

1695
        // Use AssertionHandler for assertion evaluation
1696
        let result = self.assertion_handler.evaluate_assertions_for_section(
1✔
1697
            lines_to_evaluate,
1✔
1698
            target_value,
1✔
1699
            assertion_context.headers,
1✔
1700
            assertion_context.trailers,
1✔
1701
            &context,
1✔
1702
            start_line,
1✔
1703
            assertion_context.timing,
1✔
1704
        );
1705

1706
        if !result.passed {
1✔
1707
            failure_reasons.extend(result.failure_messages);
×
1708
        }
1✔
1709
    }
1✔
1710

1711
    /// Format JSON comparison diffs and append to failure_reasons.
1712
    fn append_response_diffs(
×
1713
        &self,
×
1714
        diffs: Vec<crate::assert::AssertionResult>,
×
1715
        section_line: usize,
×
1716
        expected: &Value,
×
1717
        actual: &Value,
×
1718
        failure_reasons: &mut Vec<String>,
×
1719
    ) {
×
1720
        failure_reasons.push(format!("Response mismatch at line {}:", section_line));
×
1721
        for diff in diffs {
×
1722
            match diff {
×
1723
                crate::assert::AssertionResult::Fail {
1724
                    message,
×
1725
                    expected: exp,
×
1726
                    actual: act,
×
1727
                } => {
1728
                    let mut msg = format!("  - {}", message);
×
1729
                    if let (Some(e), Some(a)) = (exp, act) {
×
1730
                        msg.push_str(&format!("\n      Expected: {}\n      Actual:   {}", e, a));
×
1731
                    }
×
1732
                    failure_reasons.push(msg);
×
1733
                }
1734
                crate::assert::AssertionResult::Error(m) => {
×
1735
                    failure_reasons.push(format!("  - Error: {}", m));
×
1736
                }
×
1737
                _ => {}
×
1738
            }
1739
        }
1740
        failure_reasons.push(get_json_diff(expected, actual));
×
1741
    }
×
1742

1743
    /// Log a response message for debug/verbose/raw modes.
1744
    fn log_response_message(msg: &Value, effective_no_assert: bool, verbose: bool) {
×
1745
        let should_format =
×
1746
            tracing::enabled!(tracing::Level::DEBUG) || effective_no_assert || verbose;
×
1747
        if !should_format {
×
1748
            return;
×
1749
        }
×
1750
        let pretty = runner_helpers::format_json_pretty(msg);
×
1751
        if tracing::enabled!(tracing::Level::DEBUG) {
×
1752
            tracing::debug!("Received Response:\n{}", pretty);
×
1753
        }
×
1754
        if effective_no_assert {
×
1755
            println!("--- RESPONSE (Raw) ---\n{}", pretty);
×
1756
        } else if verbose {
×
1757
            println!("🔍 gRPC response received: '{}'", pretty);
×
1758
        }
×
1759
    }
×
1760

1761
    /// Print dry-run preview of test execution
1762
    fn print_dry_run_preview(
1✔
1763
        &self,
1✔
1764
        document: &GctfDocument,
1✔
1765
        address: &str,
1✔
1766
        package: &str,
1✔
1767
        service: &str,
1✔
1768
        method: &str,
1✔
1769
    ) {
1✔
1770
        println!();
1✔
1771
        println!("🔍 Dry-Run Preview: {}", document.file_path);
1✔
1772
        println!("═══════════════════════════════════════════════════════════════");
1✔
1773
        println!();
1✔
1774
        println!("📍 Target:");
1✔
1775
        println!("   Address: {}", address);
1✔
1776
        let full_service = runner_helpers::full_service_name(package, service);
1✔
1777
        println!("   Endpoint: {} / {}", full_service, method);
1✔
1778
        println!();
1✔
1779

1780
        // Display headers first
1781
        let mut has_headers = false;
1✔
1782
        for section in &document.sections {
3✔
1783
            if section.section_type == SectionType::RequestHeaders {
3✔
1784
                if !has_headers {
×
1785
                    println!();
×
1786
                    println!("📋 Request Headers:");
×
1787
                    has_headers = true;
×
1788
                }
×
1789
                if let SectionContent::KeyValues(headers) = &section.content {
×
1790
                    for (key, value) in headers {
×
1791
                        println!("   {}: {}", key, value);
×
1792
                    }
×
1793
                }
×
1794
            }
3✔
1795
        }
1796

1797
        // Group requests and responses to show flow
1798
        let mut has_request = false;
1✔
1799
        let mut has_asserts = false;
1✔
1800
        let mut has_error = false;
1✔
1801

1802
        for section in &document.sections {
3✔
1803
            match section.section_type {
3✔
1804
                SectionType::Address => {}
×
1805
                SectionType::Endpoint => {}
1✔
1806
                SectionType::RequestHeaders => {}
×
1807
                SectionType::Options => {}
×
1808
                SectionType::Tls => {}
×
1809
                SectionType::Proto => {}
×
1810
                SectionType::Request => {
1811
                    if !has_request {
1✔
1812
                        println!();
1✔
1813
                        println!("📤 Request/Response Flow:");
1✔
1814
                        has_request = true;
1✔
1815
                    }
1✔
1816
                    if let SectionContent::Json(json) = &section.content {
1✔
1817
                        let json_str = runner_helpers::format_json_pretty(json);
1✔
1818
                        println!("   ➤ REQUEST:");
1✔
1819
                        println!("     {}", json_str.replace('\n', "\n     "));
1✔
1820
                    }
1✔
1821
                }
1822
                SectionType::Response => {
1823
                    let with_asserts = if section.inline_options.with_asserts {
1✔
1824
                        " (with_asserts)"
×
1825
                    } else {
1826
                        ""
1✔
1827
                    };
1828
                    match &section.content {
1✔
1829
                        SectionContent::Json(json) => {
1✔
1830
                            let json_str = runner_helpers::format_json_pretty(json);
1✔
1831
                            println!(
1✔
1832
                                "   ↤ RESPONSE (Line {}):{}",
1✔
1833
                                section.start_line, with_asserts
1✔
1834
                            );
1✔
1835
                            println!("     {}", json_str.replace('\n', "\n     "));
1✔
1836
                        }
1✔
1837
                        SectionContent::JsonLines(values) => {
×
1838
                            println!(
×
1839
                                "   ↤ RESPONSE (Line {}, {} messages):{}",
1840
                                section.start_line,
1841
                                values.len(),
×
1842
                                with_asserts
1843
                            );
1844
                            for value in values {
×
1845
                                let json_str = runner_helpers::format_json_pretty(value);
×
1846
                                println!("     {}", json_str.replace('\n', "\n     "));
×
1847
                            }
×
1848
                        }
1849
                        _ => {}
×
1850
                    }
1851
                }
1852
                SectionType::Asserts => {
1853
                    if !has_asserts {
×
1854
                        println!();
×
1855
                        println!("✓ Assertions:");
×
1856
                        has_asserts = true;
×
1857
                    }
×
1858
                    if let SectionContent::Assertions(lines) = &section.content {
×
1859
                        for line in lines {
×
1860
                            println!("   . {}", line);
×
1861
                        }
×
1862
                    }
×
1863
                }
1864
                SectionType::Error => {
1865
                    if !has_error {
×
1866
                        println!();
×
1867
                        println!("❌ Expected Error:");
×
1868
                        has_error = true;
×
1869
                    }
×
1870
                    if let SectionContent::Json(json) = &section.content {
×
1871
                        let json_str = runner_helpers::format_json_pretty(json);
×
1872
                        println!("   {}", json_str);
×
1873
                    }
×
1874
                }
1875
                SectionType::Extract => {
1876
                    println!();
×
1877
                    println!("💾 Variables to Extract:");
×
1878
                    if let SectionContent::Extract(extractions) = &section.content {
×
1879
                        for (key, query) in extractions {
×
1880
                            println!("   {} -> {}", key, query);
×
1881
                        }
×
1882
                    }
×
1883
                }
1884
                SectionType::Meta => {}
×
1885
            }
1886
        }
1887

1888
        // Show TLS config if present (including environment defaults)
1889
        let tls_defaults = runner_helpers::tls_env_defaults();
1✔
1890
        if let Some(tls_config) = document.get_tls_config_with_defaults(&tls_defaults) {
1✔
1891
            println!();
×
1892
            println!("🔒 TLS Configuration:");
×
1893
            if let Some(ca_cert) = tls_config
×
1894
                .get("ca_cert")
×
1895
                .or_else(|| tls_config.get("ca_file"))
×
1896
            {
×
1897
                println!("   CA Cert: {}", ca_cert);
×
1898
            }
×
1899
            if let Some(client_cert) = tls_config
×
1900
                .get("client_cert")
×
1901
                .or_else(|| tls_config.get("cert"))
×
1902
                .or_else(|| tls_config.get("cert_file"))
×
1903
            {
×
1904
                println!("   Client Cert: {}", client_cert);
×
1905
            }
×
1906
            if let Some(client_key) = tls_config
×
1907
                .get("client_key")
×
1908
                .or_else(|| tls_config.get("key"))
×
1909
                .or_else(|| tls_config.get("key_file"))
×
1910
            {
×
1911
                println!("   Client Key: {}", client_key);
×
1912
            }
×
1913
            if tls_config
×
1914
                .get("insecure")
×
1915
                .map(|s| runner_helpers::parse_bool_flag(s))
×
1916
                .unwrap_or(false)
×
1917
            {
×
1918
                println!("   Insecure Skip Verify: true");
×
1919
            }
×
1920
        }
1✔
1921

1922
        // Show PROTO config if present
1923
        if let Some(proto_config) = document.get_proto_config() {
1✔
1924
            println!();
×
1925
            println!("📄 Proto Configuration:");
×
NEW
1926
            if let Some(descriptor) = proto_config.get("descriptor") {
×
NEW
1927
                println!("   Descriptor: {}", descriptor);
×
1928
            }
×
NEW
1929
            if let Some(files) = proto_config.get("files") {
×
NEW
1930
                println!("   Proto Files: {}", files);
×
UNCOV
1931
            }
×
1932
        }
1✔
1933

1934
        println!();
1✔
1935
        println!("═══════════════════════════════════════════════════════════════");
1✔
1936
        println!();
1✔
1937
    }
1✔
1938
}
1939

1940
#[cfg(test)]
1941
mod tests {
1942
    use super::*;
1943
    use crate::polyfill::runtime;
1944
    use serde_json::json;
1945
    use std::sync::Mutex;
1946

1947
    static ENV_MUTEX: Mutex<()> = Mutex::new(());
1948

1949
    #[test]
1950
    fn test_test_runner_new() {
1✔
1951
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
1952
        assert!(!runner.dry_run);
1✔
1953
        assert_eq!(runner.timeout_seconds, 30);
1✔
1954
        assert!(!runner.no_assert);
1✔
1955
        assert!(!runner.write_mode);
1✔
1956
        assert!(!runner.verbose);
1✔
1957
    }
1✔
1958

1959
    #[test]
1960
    fn test_test_runner_with_dry_run() {
1✔
1961
        let runner = TestRunner::new(true, 30, false, false, false, None);
1✔
1962
        assert!(runner.dry_run);
1✔
1963
    }
1✔
1964

1965
    #[test]
1966
    fn test_test_runner_with_timeout() {
1✔
1967
        let runner = TestRunner::new(false, 60, false, false, false, None);
1✔
1968
        assert_eq!(runner.timeout_seconds, 60);
1✔
1969
    }
1✔
1970

1971
    #[test]
1972
    fn test_test_runner_with_no_assert() {
1✔
1973
        let runner = TestRunner::new(false, 30, true, false, false, None);
1✔
1974
        assert!(runner.no_assert);
1✔
1975
    }
1✔
1976

1977
    #[test]
1978
    fn test_test_runner_with_write_mode() {
1✔
1979
        let runner = TestRunner::new(false, 30, false, true, false, None);
1✔
1980
        assert!(runner.write_mode);
1✔
1981
    }
1✔
1982

1983
    #[test]
1984
    fn test_parse_bool_flag_truthy_values() {
1✔
1985
        assert!(runner_helpers::parse_bool_flag("true"));
1✔
1986
        assert!(runner_helpers::parse_bool_flag("1"));
1✔
1987
        assert!(runner_helpers::parse_bool_flag("YES"));
1✔
1988
        assert!(runner_helpers::parse_bool_flag("on"));
1✔
1989
    }
1✔
1990

1991
    #[test]
1992
    fn test_parse_bool_flag_falsy_values() {
1✔
1993
        assert!(!runner_helpers::parse_bool_flag("false"));
1✔
1994
        assert!(!runner_helpers::parse_bool_flag("0"));
1✔
1995
        assert!(!runner_helpers::parse_bool_flag("off"));
1✔
1996
        assert!(!runner_helpers::parse_bool_flag(""));
1✔
1997
    }
1✔
1998

1999
    #[test]
2000
    fn test_parse_compression_option_from_options() {
1✔
2001
        let mut options = HashMap::new();
1✔
2002
        options.insert("compression".to_string(), "gzip".to_string());
1✔
2003

2004
        assert_eq!(
1✔
2005
            runner_helpers::parse_compression_option(&options),
1✔
2006
            crate::grpc::CompressionMode::Gzip
2007
        );
2008
    }
1✔
2009

2010
    #[test]
2011
    fn test_parse_compression_option_none_from_options() {
1✔
2012
        let mut options = HashMap::new();
1✔
2013
        options.insert("compression".to_string(), "none".to_string());
1✔
2014

2015
        assert_eq!(
1✔
2016
            runner_helpers::parse_compression_option(&options),
1✔
2017
            crate::grpc::CompressionMode::None
2018
        );
2019
    }
1✔
2020

2021
    #[test]
2022
    fn test_parse_compression_option_fallback_to_env() {
1✔
2023
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2024
        unsafe {
1✔
2025
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_COMPRESSION, "gzip");
1✔
2026
        }
1✔
2027

2028
        let mut options = HashMap::new();
1✔
2029
        options.insert("compression".to_string(), "invalid".to_string());
1✔
2030
        assert_eq!(
1✔
2031
            runner_helpers::parse_compression_option(&options),
1✔
2032
            crate::grpc::CompressionMode::Gzip
2033
        );
2034

2035
        unsafe {
1✔
2036
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_COMPRESSION);
1✔
2037
        }
1✔
2038
    }
1✔
2039

2040
    #[test]
2041
    fn test_resolve_tls_path_from_env_uses_cwd() {
1✔
2042
        if !runtime::supports(runtime::Capability::IsolatedFsIo) {
1✔
2043
            return;
×
2044
        }
1✔
2045

2046
        let cwd = std::env::current_dir().unwrap();
1✔
2047
        let document_path = Path::new("tests/fixtures/sample.gctf");
1✔
2048
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", true, document_path);
1✔
2049
        assert_eq!(Path::new(&resolved), cwd.join("certs/ca.crt"));
1✔
2050
    }
1✔
2051

2052
    #[test]
2053
    fn test_resolve_tls_path_from_env_without_fs_capability_returns_relative() {
1✔
2054
        if runtime::supports(runtime::Capability::IsolatedFsIo) {
1✔
2055
            return;
1✔
2056
        }
×
2057

2058
        let document_path = Path::new("tests/fixtures/sample.gctf");
×
2059
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", true, document_path);
×
2060
        assert_eq!(resolved, "certs/ca.crt");
×
2061
    }
1✔
2062

2063
    #[test]
2064
    fn test_resolve_tls_path_from_document_uses_document_dir() {
1✔
2065
        let document_path = Path::new("tests/fixtures/sample.gctf");
1✔
2066
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", false, document_path);
1✔
2067
        assert_eq!(
1✔
2068
            Path::new(&resolved),
1✔
2069
            Path::new("tests/fixtures").join("certs").join("ca.crt")
1✔
2070
        );
2071
    }
1✔
2072

2073
    #[test]
2074
    fn test_tls_env_defaults_uses_grpctestify_prefix() {
1✔
2075
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2076

2077
        unsafe {
1✔
2078
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE, "/tmp/ca.pem");
1✔
2079
            std::env::set_var(
1✔
2080
                crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE,
1✔
2081
                "/tmp/cert.pem",
1✔
2082
            );
1✔
2083
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE, "/tmp/key.pem");
1✔
2084
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME, "localhost");
1✔
2085
        }
1✔
2086

2087
        let defaults = runner_helpers::tls_env_defaults();
1✔
2088
        assert_eq!(defaults.get("ca_cert"), Some(&"/tmp/ca.pem".to_string()));
1✔
2089
        assert_eq!(
1✔
2090
            defaults.get("client_cert"),
1✔
2091
            Some(&"/tmp/cert.pem".to_string())
1✔
2092
        );
2093
        assert_eq!(
1✔
2094
            defaults.get("client_key"),
1✔
2095
            Some(&"/tmp/key.pem".to_string())
1✔
2096
        );
2097
        assert_eq!(defaults.get("server_name"), Some(&"localhost".to_string()));
1✔
2098

2099
        unsafe {
1✔
2100
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE);
1✔
2101
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE);
1✔
2102
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE);
1✔
2103
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME);
1✔
2104
        }
1✔
2105
    }
1✔
2106

2107
    #[test]
2108
    fn test_tls_env_defaults_ignores_empty_values() {
1✔
2109
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2110

2111
        unsafe {
1✔
2112
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE, "");
1✔
2113
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE, "   ");
1✔
2114
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE, "");
1✔
2115
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME, " ");
1✔
2116
        }
1✔
2117

2118
        let defaults = runner_helpers::tls_env_defaults();
1✔
2119
        assert!(defaults.is_empty());
1✔
2120

2121
        unsafe {
1✔
2122
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE);
1✔
2123
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE);
1✔
2124
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE);
1✔
2125
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME);
1✔
2126
        }
1✔
2127
    }
1✔
2128

2129
    #[test]
2130
    fn test_test_runner_with_verbose() {
1✔
2131
        let runner = TestRunner::new(false, 30, false, false, true, None);
1✔
2132
        assert!(runner.verbose);
1✔
2133
    }
1✔
2134

2135
    #[test]
2136
    fn test_grpc_code_name_from_numeric() {
1✔
2137
        assert_eq!(TestRunner::grpc_code_name_from_numeric(0), Some("OK"));
1✔
2138
        assert_eq!(TestRunner::grpc_code_name_from_numeric(5), Some("NotFound"));
1✔
2139
        assert_eq!(
1✔
2140
            TestRunner::grpc_code_name_from_numeric(13),
1✔
2141
            Some("Internal")
2142
        );
2143
        assert_eq!(TestRunner::grpc_code_name_from_numeric(99), None);
1✔
2144
    }
1✔
2145

2146
    #[test]
2147
    fn test_error_matches_expected_message() {
1✔
2148
        let expected = json!({
1✔
2149
            "message": "Can't find stub",
1✔
2150
            "code": 5
1✔
2151
        });
2152
        let error_text = "status: NotFound, message: \"Can't find stub\"";
1✔
2153
        assert!(TestRunner::error_matches_expected(error_text, &expected));
1✔
2154
    }
1✔
2155

2156
    #[test]
2157
    fn test_error_matches_expected_code() {
1✔
2158
        let expected = json!({
1✔
2159
            "code": 5
1✔
2160
        });
2161
        let error_text = "status: NotFound, message: \"error\"";
1✔
2162
        assert!(TestRunner::error_matches_expected(error_text, &expected));
1✔
2163
    }
1✔
2164

2165
    #[test]
2166
    fn test_error_matches_expected_wrong_code() {
1✔
2167
        let expected = json!({
1✔
2168
            "code": 3
1✔
2169
        });
2170
        let error_text = "status: NotFound, message: \"error\"";
1✔
2171
        assert!(!TestRunner::error_matches_expected(error_text, &expected));
1✔
2172
    }
1✔
2173

2174
    #[test]
2175
    fn test_error_matches_expected_wrong_message() {
1✔
2176
        let expected = json!({
1✔
2177
            "message": "Different error"
1✔
2178
        });
2179
        let error_text = "status: NotFound, message: \"Can't find stub\"";
1✔
2180
        assert!(!TestRunner::error_matches_expected(error_text, &expected));
1✔
2181
    }
1✔
2182

2183
    #[test]
2184
    fn test_error_matches_expected_string() {
1✔
2185
        let expected = json!("Can't find stub");
1✔
2186
        let error_text = "status: NotFound, message: \"Can't find stub\"";
1✔
2187
        assert!(TestRunner::error_matches_expected(error_text, &expected));
1✔
2188
    }
1✔
2189

2190
    #[test]
2191
    fn test_full_service_name() {
1✔
2192
        assert_eq!(
1✔
2193
            runner_helpers::full_service_name("package", "Service"),
1✔
2194
            "package.Service"
2195
        );
2196
        assert_eq!(runner_helpers::full_service_name("", "Service"), "Service");
1✔
2197
    }
1✔
2198

2199
    #[test]
2200
    fn test_substitute_variables_exact_match_preserves_type() {
1✔
2201
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2202
        let mut value = json!("{{ count }}");
1✔
2203
        let mut vars = HashMap::new();
1✔
2204
        vars.insert("count".to_string(), json!(42));
1✔
2205

2206
        runner.substitute_variables(&mut value, &vars);
1✔
2207
        assert_eq!(value, json!(42));
1✔
2208
    }
1✔
2209

2210
    #[test]
2211
    fn test_substitute_variables_interpolation_single_pass() {
1✔
2212
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2213
        let mut value = json!("id={{id}}, user={{ user }}, ok={{ok}}");
1✔
2214
        let mut vars = HashMap::new();
1✔
2215
        vars.insert("id".to_string(), json!(7));
1✔
2216
        vars.insert("user".to_string(), json!("alice"));
1✔
2217
        vars.insert("ok".to_string(), json!(true));
1✔
2218

2219
        runner.substitute_variables(&mut value, &vars);
1✔
2220
        assert_eq!(value, json!("id=7, user=alice, ok=true"));
1✔
2221
    }
1✔
2222

2223
    #[test]
2224
    fn test_substitute_variables_keeps_unknown_placeholder() {
1✔
2225
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2226
        let mut value = json!("hello {{known}} and {{unknown}}");
1✔
2227
        let mut vars = HashMap::new();
1✔
2228
        vars.insert("known".to_string(), json!("world"));
1✔
2229

2230
        runner.substitute_variables(&mut value, &vars);
1✔
2231
        assert_eq!(value, json!("hello world and {{unknown}}"));
1✔
2232
    }
1✔
2233

2234
    #[test]
2235
    fn test_expected_values_for_response_section() {
1✔
2236
        use crate::parser::ast::{InlineOptions, Section, SectionContent};
2237

2238
        let section = Section {
1✔
2239
            section_type: crate::parser::ast::SectionType::Response,
1✔
2240
            content: SectionContent::Json(json!({"key": "value"})),
1✔
2241
            inline_options: InlineOptions::default(),
1✔
2242
            raw_content: "".to_string(),
1✔
2243
            start_line: 1,
1✔
2244
            end_line: 2,
1✔
2245
        };
1✔
2246

2247
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2248
        assert_eq!(values.len(), 1);
1✔
2249
        assert_eq!(values[0], json!({"key": "value"}));
1✔
2250
    }
1✔
2251

2252
    #[test]
2253
    fn test_expected_values_for_json_lines() {
1✔
2254
        use crate::parser::ast::{InlineOptions, Section, SectionContent};
2255

2256
        let section = Section {
1✔
2257
            section_type: crate::parser::ast::SectionType::Response,
1✔
2258
            content: SectionContent::JsonLines(vec![
1✔
2259
                json!({"key1": "value1"}),
1✔
2260
                json!({"key2": "value2"}),
1✔
2261
            ]),
1✔
2262
            inline_options: InlineOptions::default(),
1✔
2263
            raw_content: "".to_string(),
1✔
2264
            start_line: 1,
1✔
2265
            end_line: 3,
1✔
2266
        };
1✔
2267

2268
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2269
        assert_eq!(values.len(), 2);
1✔
2270
    }
1✔
2271

2272
    #[test]
2273
    fn test_expected_values_for_other_section() {
1✔
2274
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2275

2276
        // The function returns values for any Json content, not just Response sections
2277
        // This is expected behavior - it extracts Json values regardless of section type
2278
        let section = Section {
1✔
2279
            section_type: SectionType::Request,
1✔
2280
            content: SectionContent::Json(json!({"key": "value"})),
1✔
2281
            inline_options: InlineOptions::default(),
1✔
2282
            raw_content: "".to_string(),
1✔
2283
            start_line: 1,
1✔
2284
            end_line: 2,
1✔
2285
        };
1✔
2286

2287
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2288
        // Returns 1 because the content is Json, even though it's a Request section
2289
        assert_eq!(values.len(), 1);
1✔
2290
        assert_eq!(values[0], json!({"key": "value"}));
1✔
2291
    }
1✔
2292

2293
    #[test]
2294
    fn test_metadata_map_to_hashmap_extracts_ascii_values() {
1✔
2295
        let mut metadata = tonic::metadata::MetadataMap::new();
1✔
2296
        metadata.insert("code", "EXTERNAL_SERVICE_ERROR_CODE".parse().unwrap());
1✔
2297
        metadata.insert("message", "External service error message".parse().unwrap());
1✔
2298

2299
        let trailers = runner_helpers::metadata_map_to_hashmap(&metadata);
1✔
2300
        assert_eq!(
1✔
2301
            trailers.get("code"),
1✔
2302
            Some(&"EXTERNAL_SERVICE_ERROR_CODE".to_string())
1✔
2303
        );
2304
        assert_eq!(
1✔
2305
            trailers.get("message"),
1✔
2306
            Some(&"External service error message".to_string())
1✔
2307
        );
2308
    }
1✔
2309

2310
    #[test]
2311
    fn test_assertion_scope_timing_single_message_scope() {
1✔
2312
        let mut timing = AssertionScopeTimingState::default();
1✔
2313

2314
        let first = timing.finish_scope(0, 12, 1).unwrap();
1✔
2315

2316
        assert_eq!(first.elapsed_ms, 12);
1✔
2317
        assert_eq!(first.total_elapsed_ms, 12);
1✔
2318
        assert_eq!(first.scope_message_count, 1);
1✔
2319
        assert_eq!(first.scope_index, 1);
1✔
2320
    }
1✔
2321

2322
    #[test]
2323
    fn test_assertion_scope_timing_batch_scope_uses_full_section_window() {
1✔
2324
        let mut timing = AssertionScopeTimingState::default();
1✔
2325

2326
        let batch = timing.finish_scope(0, 27, 2).unwrap();
1✔
2327

2328
        assert_eq!(batch.elapsed_ms, 27);
1✔
2329
        assert_eq!(batch.total_elapsed_ms, 27);
1✔
2330
        assert_eq!(batch.scope_message_count, 2);
1✔
2331
        assert_eq!(batch.scope_index, 1);
1✔
2332
    }
1✔
2333

2334
    #[test]
2335
    fn test_assertion_scope_timing_accumulates_total_duration() {
1✔
2336
        let mut timing = AssertionScopeTimingState::default();
1✔
2337

2338
        let first = timing.finish_scope(0, 10, 1).unwrap();
1✔
2339
        let second = timing.finish_scope(10, 35, 3).unwrap();
1✔
2340

2341
        assert_eq!(first.elapsed_ms, 10);
1✔
2342
        assert_eq!(first.total_elapsed_ms, 10);
1✔
2343
        assert_eq!(second.elapsed_ms, 25);
1✔
2344
        assert_eq!(second.total_elapsed_ms, 35);
1✔
2345
        assert_eq!(second.scope_message_count, 3);
1✔
2346
        assert_eq!(second.scope_index, 2);
1✔
2347
    }
1✔
2348

2349
    #[test]
2350
    fn test_has_required_followup_asserts_for_error_requires_adjacent_asserts() {
1✔
2351
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2352

2353
        let error = Section {
1✔
2354
            section_type: SectionType::Error,
1✔
2355
            content: SectionContent::Empty,
1✔
2356
            inline_options: InlineOptions {
1✔
2357
                with_asserts: true,
1✔
2358
                ..InlineOptions::default()
1✔
2359
            },
1✔
2360
            raw_content: "".to_string(),
1✔
2361
            start_line: 12,
1✔
2362
            end_line: 12,
1✔
2363
        };
1✔
2364
        let sections = vec![error.clone()];
1✔
2365
        let mut failures = Vec::new();
1✔
2366

2367
        let has_followup =
1✔
2368
            TestRunner::has_required_followup_asserts(&error, &sections, 0, false, &mut failures);
1✔
2369

2370
        assert!(!has_followup);
1✔
2371
        assert_eq!(failures.len(), 1);
1✔
2372
        assert!(failures[0].contains("ERROR at line 12 has 'with_asserts'"));
1✔
2373
    }
1✔
2374

2375
    #[test]
2376
    fn test_has_required_followup_asserts_for_error_accepts_adjacent_asserts() {
1✔
2377
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2378

2379
        let error = Section {
1✔
2380
            section_type: SectionType::Error,
1✔
2381
            content: SectionContent::Empty,
1✔
2382
            inline_options: InlineOptions {
1✔
2383
                with_asserts: true,
1✔
2384
                ..InlineOptions::default()
1✔
2385
            },
1✔
2386
            raw_content: "".to_string(),
1✔
2387
            start_line: 20,
1✔
2388
            end_line: 20,
1✔
2389
        };
1✔
2390
        let asserts = Section {
1✔
2391
            section_type: SectionType::Asserts,
1✔
2392
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
2393
            inline_options: InlineOptions::default(),
1✔
2394
            raw_content: ".code == 5".to_string(),
1✔
2395
            start_line: 21,
1✔
2396
            end_line: 21,
1✔
2397
        };
1✔
2398
        let sections = vec![error.clone(), asserts];
1✔
2399
        let mut failures = Vec::new();
1✔
2400

2401
        let has_followup =
1✔
2402
            TestRunner::has_required_followup_asserts(&error, &sections, 0, false, &mut failures);
1✔
2403

2404
        assert!(has_followup);
1✔
2405
        assert!(failures.is_empty());
1✔
2406
    }
1✔
2407

2408
    #[test]
2409
    fn test_error_assertions_evaluate_against_error_json_object() {
1✔
2410
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2411
        let target = json!({
1✔
2412
            "code": 5,
1✔
2413
            "message": "resource not found in backend",
1✔
2414
            "details": [
1✔
2415
                {
2416
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
2417
                    "reason": "NOT_FOUND"
1✔
2418
                }
2419
            ]
2420
        });
2421
        let lines = vec![
1✔
2422
            ".code == 5".to_string(),
1✔
2423
            ".message contains \"not found\"".to_string(),
1✔
2424
            ".details[0][\"@type\"] == \"type.googleapis.com/google.rpc.ErrorInfo\"".to_string(),
1✔
2425
        ];
2426
        let mut failures = Vec::new();
1✔
2427
        let headers: HashMap<String, String> = HashMap::new();
1✔
2428
        let trailers: HashMap<String, String> = HashMap::new();
1✔
2429

2430
        runner.run_assertions(
1✔
2431
            &lines,
1✔
2432
            &target,
1✔
2433
            &mut failures,
1✔
2434
            "(attached to ERROR at line 1)".to_string(),
1✔
2435
            1,
2436
            AssertionContext {
1✔
2437
                headers: &headers,
1✔
2438
                trailers: &trailers,
1✔
2439
                timing: None,
1✔
2440
            },
1✔
2441
        );
2442

2443
        assert!(failures.is_empty(), "unexpected failures: {failures:?}");
1✔
2444
    }
1✔
2445
}
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