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

gripmock / grpctestify-rust / 24849353019

23 Apr 2026 05:30PM UTC coverage: 77.721% (+0.8%) from 76.897%
24849353019

Pull #42

github

web-flow
Merge 43a0b7af9 into 59e77d08a
Pull Request #42: bump rustc & command grpcurl (exp)

1068 of 1260 new or added lines in 25 files covered. (84.76%)

95 existing lines in 7 files now uncovered.

18862 of 24269 relevant lines covered (77.72%)

40957.95 hits per line

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

46.19
/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
/// Buffer size for the request message channel.
25
/// Controls back-pressure for client streaming: larger values allow more
26
/// buffered requests but consume more memory.
27
const REQUEST_CHANNEL_BUFFER: usize = 100;
28

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

44
#[derive(Debug, Clone, Serialize, Deserialize)]
45
pub struct ConnectionInfo {
46
    pub address: String,
47
    pub source: String,
48
}
49

50
#[derive(Debug, Clone, Serialize, Deserialize)]
51
pub struct TargetInfo {
52
    pub endpoint: String,
53
    pub package: Option<String>,
54
    pub service: Option<String>,
55
    pub method: Option<String>,
56
}
57

58
#[derive(Debug, Clone, Serialize, Deserialize)]
59
pub struct HeadersInfo {
60
    pub count: usize,
61
    pub headers: HashMap<String, String>,
62
}
63

64
#[derive(Debug, Clone, Serialize, Deserialize)]
65
pub struct RequestInfo {
66
    pub index: usize,
67
    pub content: Value,
68
    pub content_type: String,
69
    pub line_start: usize,
70
    pub line_end: usize,
71
}
72

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

84
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85
pub struct ComparisonOptions {
86
    pub partial: bool,
87
    pub redact: Vec<String>,
88
    pub tolerance: Option<f64>,
89
    pub unordered_arrays: bool,
90
    pub with_asserts: bool,
91
}
92

93
#[derive(Debug, Clone, Serialize, Deserialize)]
94
pub struct AssertionInfo {
95
    pub index: usize,
96
    pub assertions: Vec<String>,
97
    pub line_start: usize,
98
    pub line_end: usize,
99
}
100

101
#[derive(Debug, Clone, Serialize, Deserialize)]
102
pub struct ExtractionInfo {
103
    pub index: usize,
104
    pub variables: HashMap<String, String>,
105
    pub line_start: usize,
106
    pub line_end: usize,
107
}
108

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

127
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
128
pub struct ExecutionSummary {
129
    pub total_requests: usize,
130
    pub total_responses: usize,
131
    pub total_errors: usize,
132
    pub error_expected: bool,
133
    pub assertion_blocks: usize,
134
    pub variable_extractions: usize,
135
    pub rpc_mode_name: String,
136
}
137

138
struct AssertionContext<'a> {
139
    headers: &'a HashMap<String, String>,
140
    trailers: &'a HashMap<String, String>,
141
    timing: Option<&'a AssertionTiming>,
142
}
143

144
impl ExecutionPlan {
145
    /// Build execution plan from a GctfDocument
146
    pub fn from_document(doc: &GctfDocument) -> Self {
77✔
147
        let file_path = doc.file_path.clone();
77✔
148

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

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

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

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

238
        // Expectations (responses or error)
239
        let response_sections = doc.sections_by_type(SectionType::Response);
77✔
240
        let error_section = doc.first_section(SectionType::Error);
77✔
241

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

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

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

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

350
        // Summary
351
        let rpc_mode_name = match &rpc_mode {
77✔
352
            RpcMode::Unary => "Unary",
61✔
353
            RpcMode::UnaryError => "Unary Error",
5✔
354
            RpcMode::ServerStreaming { .. } => "Server Streaming",
5✔
355
            RpcMode::ClientStreaming { .. } => "Client Streaming",
5✔
356
            RpcMode::BidirectionalStreaming { .. } => "Bidirectional Streaming",
×
357
            RpcMode::Unknown => "Unknown",
1✔
358
        };
359

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

376
        ExecutionPlan {
77✔
377
            file_path,
77✔
378
            connection,
77✔
379
            target,
77✔
380
            headers,
77✔
381
            requests,
77✔
382
            expectations,
77✔
383
            assertions,
77✔
384
            extractions,
77✔
385
            rpc_mode,
77✔
386
            summary,
77✔
387
        }
77✔
388
    }
77✔
389
}
390

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

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

431
/// Test execution status
432
#[derive(Debug, Clone, PartialEq, Eq)]
433
pub enum TestExecutionStatus {
434
    Pass,
435
    Fail(String),
436
}
437

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

447
#[derive(Debug, Default, Clone)]
448
struct AssertionScopeTimingState {
449
    last_message_elapsed_ms: Option<u64>,
450
    total_scope_elapsed_ms: u64,
451
    scope_index: usize,
452
}
453

454
impl AssertionScopeTimingState {
455
    fn finish_scope(
4✔
456
        &mut self,
4✔
457
        scope_start_ms: u64,
4✔
458
        scope_end_ms: u64,
4✔
459
        scope_message_count: usize,
4✔
460
    ) -> Option<AssertionTiming> {
4✔
461
        if scope_message_count == 0 {
4✔
462
            return None;
×
463
        }
4✔
464

465
        let elapsed_ms = scope_end_ms.saturating_sub(scope_start_ms);
4✔
466
        self.scope_index += 1;
4✔
467
        self.total_scope_elapsed_ms = self.total_scope_elapsed_ms.saturating_add(elapsed_ms);
4✔
468

469
        let timing = AssertionTiming {
4✔
470
            elapsed_ms,
4✔
471
            total_elapsed_ms: self.total_scope_elapsed_ms,
4✔
472
            scope_message_count,
4✔
473
            scope_index: self.scope_index,
4✔
474
        };
4✔
475

476
        Some(timing)
4✔
477
    }
4✔
478
}
479

480
impl TestExecutionResult {
481
    pub fn pass(grpc_duration_ms: Option<u64>) -> Self {
10✔
482
        Self {
10✔
483
            status: TestExecutionStatus::Pass,
10✔
484
            grpc_duration_ms,
10✔
485
            captured_response: None,
10✔
486
        }
10✔
487
    }
10✔
488

489
    pub fn fail(message: String, grpc_duration_ms: Option<u64>) -> Self {
7✔
490
        Self {
7✔
491
            status: TestExecutionStatus::Fail(message),
7✔
492
            grpc_duration_ms,
7✔
493
            captured_response: None,
7✔
494
        }
7✔
495
    }
7✔
496

497
    pub fn with_response(mut self, response: crate::grpc::GrpcResponse) -> Self {
×
498
        self.captured_response = Some(response);
×
499
        self
×
500
    }
×
501
}
502

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

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

530
    pub fn grpc_code_name_from_numeric(code: i64) -> Option<&'static str> {
4✔
531
        super::error_handler::ErrorHandler::grpc_code_name_from_numeric(code)
4✔
532
    }
4✔
533

534
    pub fn error_matches_expected(error_text: &str, expected: &Value) -> bool {
5✔
535
        super::error_handler::ErrorHandler::error_matches_expected(error_text, expected)
5✔
536
    }
5✔
537

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

549
        if sections
2✔
550
            .get(index + 1)
2✔
551
            .is_some_and(|next| next.section_type == SectionType::Asserts)
2✔
552
        {
553
            return true;
1✔
554
        }
1✔
555

556
        if !effective_no_assert {
1✔
557
            failure_reasons.push(format!(
1✔
558
                "{} at line {} has 'with_asserts' but is not followed by ASSERTS",
1✔
559
                section.section_type.as_str(),
1✔
560
                section.start_line
1✔
561
            ));
1✔
562
        }
1✔
563

564
        false
1✔
565
    }
2✔
566

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

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

599
        for doc in document.iter_chain() {
1✔
600
            let result = self.run_one(doc, &mut variables).await?;
1✔
601
            if let Some(dur) = result.grpc_duration_ms {
1✔
602
                total_duration_ms += dur as f64;
×
603
            }
1✔
604

605
            if let TestExecutionStatus::Fail(msg) = &result.status {
1✔
606
                overall_status = TestExecutionStatus::Fail(msg.clone());
×
607
                break;
×
608
            }
1✔
609
        }
610

611
        // Build summary result
612
        Ok(TestExecutionResult {
1✔
613
            status: overall_status,
1✔
614
            grpc_duration_ms: Some(total_duration_ms as u64),
1✔
615
            captured_response: None,
1✔
616
        })
1✔
617
    }
1✔
618

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

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

646
        let compression = runner_helpers::parse_compression_option(&options);
1✔
647

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

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

668
        // Extract address
669
        let address = runner_helpers::effective_address(document);
1✔
670

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

682
        if document.sections.is_empty() {
1✔
683
            return Ok(TestExecutionResult::fail(
×
684
                "No sections found".to_string(),
×
685
                None,
×
686
            ));
×
687
        }
1✔
688

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

695
        // Configure Client
696
        let document_path = Path::new(&document.file_path);
×
697

NEW
698
        let tls_config = runner_helpers::build_tls_config(document, document_path);
×
699

700
        // Check for Proto config in document
NEW
701
        let proto_config = runner_helpers::build_proto_config(document, document_path);
×
702

703
        let full_service = runner_helpers::full_service_name(&package, &service);
×
704

705
        let client_config = GrpcClientConfig {
×
706
            address,
×
707
            timeout_seconds: effective_timeout_seconds,
×
708
            tls_config,
×
709
            proto_config,
×
710
            metadata: document.get_request_headers(),
×
711
            target_service: Some(full_service.clone()),
×
712
            compression,
×
713
        };
×
714

715
        let client = GrpcClient::new(client_config).await?;
×
716

717
        // Get input/output message types for field coverage tracking
718
        let input_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.input().full_name().to_string());
×
723
        let output_message_type = client
×
724
            .descriptor_pool()
×
725
            .get_service_by_name(&full_service)
×
726
            .and_then(|s| s.methods().find(|m| m.name() == method))
×
727
            .map(|m| m.output().full_name().to_string());
×
728

729
        // Setup Streaming
730
        let (tx, rx) = mpsc::channel::<Value>(REQUEST_CHANNEL_BUFFER);
×
731
        let request_stream = ReceiverStream::new(rx);
×
732
        let mut tx = Some(tx);
×
733

734
        // Coverage: Register pool and record call
735
        if let Some(collector) = &self.coverage_collector {
×
736
            collector.register_pool(client.descriptor_pool());
×
737
            collector.record_call(&full_service, &method);
×
738
        }
×
739

740
        let start_time = std::time::Instant::now();
×
741

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

753
        let mut response_stream = None;
×
754

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

765
        // Iterator for sections
766
        // We iterate by index to allow lookahead
767
        let sections = &document.sections;
×
768

769
        // Pre-compute last request index to avoid O(n²) lookups in loop
770
        let last_request_idx = sections
×
771
            .iter()
×
772
            .rposition(|s| s.section_type == SectionType::Request);
×
773

774
        let has_request_sections = sections
×
775
            .iter()
×
776
            .any(|s| s.section_type == SectionType::Request);
×
777

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

787
        let mut skip_next_section = false;
×
788

789
        // Capture full response for write mode
790
        let mut captured_response = if effective_write_mode {
×
791
            Some(crate::grpc::GrpcResponse::new())
×
792
        } else {
793
            None
×
794
        };
795

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

827
        for (i, section) in sections.iter().enumerate() {
×
828
            if skip_next_section {
×
829
                skip_next_section = false;
×
830
                continue;
×
831
            }
×
832

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

846
                    // Coverage: record request fields
847
                    if let (Some(collector), Some(msg_type)) =
×
848
                        (&self.coverage_collector, &input_message_type)
×
849
                    {
×
850
                        collector.record_fields_from_json(msg_type, &request_value);
×
851
                    }
×
852

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

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

877
                    if i >= last_request_idx.unwrap_or(usize::MAX) {
×
878
                        drop(tx.take());
×
879
                    }
×
880

881
                    ensure_stream_ready!();
×
882

883
                    let mut received_messages_for_section: Vec<Value> = Vec::new();
×
884
                    let expected_values = Self::expected_values_for_response_section(section);
×
885

886
                    for expected_template in expected_values {
×
887
                        match response_stream.as_mut().unwrap().next().await {
×
888
                            Some(Ok(item)) => {
×
889
                                match item {
×
890
                                    crate::grpc::client::StreamItem::Message(msg) => {
×
891
                                        let now_elapsed_ms =
×
892
                                            start_time.elapsed().as_millis() as u64;
×
893

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

908
                                        Self::log_response_message(
×
909
                                            &msg,
×
910
                                            effective_no_assert,
×
911
                                            self.verbose,
×
912
                                        );
913

914
                                        if !effective_no_assert {
×
915
                                            let mut expected = expected_template.clone();
×
916
                                            self.substitute_variables(&mut expected, variables);
×
917

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

926
                                            let diffs = JsonComparator::compare(
×
927
                                                &msg,
×
928
                                                &expected,
×
929
                                                &section.inline_options,
×
930
                                            );
931

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

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

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

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

1042
                    ensure_stream_ready!();
×
1043

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

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

1097
                            last_message = Some(msg.clone());
×
1098

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

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

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

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

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

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

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

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

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

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

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

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

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

1398
                    // Expect an error from the stream
1399
                    match response_stream.as_mut().unwrap().next().await {
×
1400
                        Some(Err(status)) => {
×
1401
                            let scope_start_ms =
×
1402
                                assertion_timing.last_message_elapsed_ms.unwrap_or(0);
×
1403
                            let scope_end_ms = start_time.elapsed().as_millis() as u64;
×
1404
                            assertion_timing.last_message_elapsed_ms = Some(scope_end_ms);
×
1405
                            let error_scope_timing =
×
1406
                                assertion_timing.finish_scope(scope_start_ms, scope_end_ms, 1);
×
1407

1408
                            let status_message = status.message();
×
1409
                            last_error_message = Some(status_message.to_string());
×
1410
                            let error_json =
×
1411
                                super::error_handler::ErrorHandler::status_to_json(&status);
×
1412
                            last_error_json = Some(error_json.clone());
×
1413
                            last_error_timing = error_scope_timing;
×
1414
                            captured_trailers
×
1415
                                .extend(runner_helpers::metadata_map_to_hashmap(status.metadata()));
×
1416
                            let should_format_error = effective_no_assert || self.verbose;
×
1417
                            let got = should_format_error.then(|| {
×
1418
                                let status_name =
×
1419
                                    Self::grpc_code_name_from_numeric(status.code() as i64)
×
1420
                                        .unwrap_or("Unknown");
×
1421
                                format!("status: {}, message: \"{}\"", status_name, status_message)
×
1422
                            });
×
1423

1424
                            if effective_no_assert {
×
1425
                                println!("--- RESPONSE (Error) ---");
×
1426
                                if let Some(got) = got.as_deref() {
×
1427
                                    println!("{}", got);
×
1428
                                }
×
1429
                            } else if self.verbose {
×
1430
                                if let Some(got) = got.as_deref() {
×
1431
                                    println!("🔍 gRPC error received: '{}'", got);
×
1432
                                }
×
1433
                                let details_json =
×
1434
                                    super::error_handler::ErrorHandler::status_details_json(
×
1435
                                        &status,
×
1436
                                    );
1437
                                if details_json != Value::Null
×
1438
                                    && details_json.as_array().is_some_and(|arr| !arr.is_empty())
×
1439
                                {
×
1440
                                    println!("🔍 gRPC error details: {}", details_json);
×
1441
                                }
×
1442
                            }
×
1443

1444
                            if !effective_no_assert {
×
1445
                                if let SectionContent::Json(expected_json) = &section.content {
×
1446
                                    let mut expected = expected_json.clone();
×
1447
                                    self.substitute_variables(&mut expected, variables);
×
1448

1449
                                    if !super::error_handler::ErrorHandler::status_matches_expected_with_options(
×
1450
                                        &status,
×
1451
                                        &expected,
×
1452
                                        section.inline_options.partial,
×
1453
                                    ) {
×
1454
                                        failure_reasons.push(format!(
×
1455
                                            "Error mismatch at line {}:",
1456
                                            section.start_line
1457
                                        ));
1458
                                        if let Some(reason) =
×
1459
                                            super::error_handler::ErrorHandler::status_mismatch_reason_with_options(
×
1460
                                                &status,
×
1461
                                                &expected,
×
1462
                                                section.inline_options.partial,
×
1463
                                            )
×
1464
                                        {
×
1465
                                            failure_reasons.push(format!("  - {}", reason));
×
1466
                                        }
×
1467
                                        let actual_json =
×
1468
                                            super::error_handler::ErrorHandler::status_to_json(
×
1469
                                                &status,
×
1470
                                            );
1471
                                        failure_reasons
×
1472
                                            .push(get_json_diff(&expected, &actual_json));
×
1473
                                    }
×
1474
                                }
×
1475

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

1542
        // Ensure we close the request stream
1543
        drop(tx.take());
×
1544

1545
        // If in update mode, capture any remaining responses
1546
        if let Some(resp) = &mut captured_response {
×
1547
            if response_stream.is_none()
×
1548
                && let Some(handle) = call_handle.take()
×
1549
            {
1550
                match handle.await {
×
1551
                    Ok(Ok((h, stream))) => {
×
1552
                        resp.headers = h;
×
1553
                        response_stream = Some(stream);
×
1554
                    }
×
1555
                    Ok(Err(_)) | Err(_) => {
×
1556
                        response_stream = None;
×
1557
                    }
×
1558
                }
1559
            }
×
1560

1561
            loop {
1562
                let next_item = if let Some(stream) = response_stream.as_mut() {
×
1563
                    stream.next().await
×
1564
                } else {
1565
                    None
×
1566
                };
1567

1568
                let Some(item_res) = next_item else {
×
1569
                    break;
×
1570
                };
1571

1572
                match item_res {
×
1573
                    Ok(crate::grpc::client::StreamItem::Message(msg)) => {
×
1574
                        resp.messages.push(msg);
×
1575
                    }
×
1576
                    Ok(crate::grpc::client::StreamItem::Trailers(t)) => {
×
1577
                        resp.trailers.extend(t);
×
1578
                    }
×
1579
                    Err(status) => {
×
1580
                        resp.error = Some(status.message().to_string());
×
1581
                    }
×
1582
                }
1583
            }
1584
        }
×
1585

1586
        let grpc_duration = start_time.elapsed().as_millis() as u64;
×
1587

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

1601
            return Ok(TestExecutionResult::fail(
×
1602
                format!("Validation failed:\n  - {}", failure_reasons.join("\n  - ")),
×
1603
                Some(grpc_duration),
×
1604
            ));
×
1605
        }
×
1606

1607
        let mut result = TestExecutionResult::pass(Some(grpc_duration));
×
1608
        if let Some(resp) = captured_response {
×
1609
            result = result.with_response(resp);
×
1610
        }
×
1611
        Ok(result)
×
1612
    }
1✔
1613

1614
    /// Validates a collected response against the document (for testing purposes)
1615
    pub fn validate_response(
12✔
1616
        &self,
12✔
1617
        document: &GctfDocument,
12✔
1618
        response: &crate::grpc::GrpcResponse,
12✔
1619
    ) -> TestExecutionResult {
12✔
1620
        self.response_handler.validate_document(document, response)
12✔
1621
    }
12✔
1622

1623
    fn substitute_variables(&self, value: &mut Value, variables: &HashMap<String, Value>) {
3✔
1624
        match value {
3✔
1625
            Value::String(s) => {
3✔
1626
                if !s.contains("{{") {
3✔
1627
                    return;
×
1628
                }
3✔
1629

1630
                // Check for exact match "{{ var }}" to preserve type
1631
                if s.starts_with("{{") && s.ends_with("}}") {
3✔
1632
                    let inner = s[2..s.len() - 2].trim();
1✔
1633
                    // check if inner has more {{ }} which implies complex string
1634
                    if !inner.contains("{{")
1✔
1635
                        && let Some(val) = variables.get(inner)
1✔
1636
                    {
1637
                        *value = val.clone();
1✔
1638
                        return;
1✔
1639
                    }
×
1640
                }
2✔
1641

1642
                // String interpolation "prefix {{ var }} suffix"
1643
                if let Some(result) = runner_helpers::interpolate_variables(s, variables) {
2✔
1644
                    *value = Value::String(result);
2✔
1645
                }
2✔
1646
            }
1647
            Value::Array(arr) => {
×
1648
                for v in arr {
×
1649
                    self.substitute_variables(v, variables);
×
1650
                }
×
1651
            }
1652
            Value::Object(obj) => {
×
1653
                for v in obj.values_mut() {
×
1654
                    self.substitute_variables(v, variables);
×
1655
                }
×
1656
            }
1657
            _ => {}
×
1658
        }
1659
    }
3✔
1660

1661
    fn run_assertions(
1✔
1662
        &self,
1✔
1663
        lines: &[String],
1✔
1664
        target_value: &Value,
1✔
1665
        failure_reasons: &mut Vec<String>,
1✔
1666
        context: String,
1✔
1667
        start_line: usize,
1✔
1668
        assertion_context: AssertionContext<'_>,
1✔
1669
    ) {
1✔
1670
        let mut optimized_lines: Option<Vec<String>> = None;
1✔
1671

1672
        for (idx, line) in lines.iter().enumerate() {
3✔
1673
            if let Some(rewritten) =
×
1674
                optimizer::rewrite_assertion_expression_fixed_point_if_changed(line)
3✔
1675
            {
1676
                let vec = optimized_lines.get_or_insert_with(|| lines[..idx].to_vec());
×
1677
                vec.push(rewritten);
×
1678
            } else if let Some(vec) = optimized_lines.as_mut() {
3✔
1679
                vec.push(line.clone());
×
1680
            }
3✔
1681
        }
1682

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

1685
        // Use AssertionHandler for assertion evaluation
1686
        let result = self.assertion_handler.evaluate_assertions_for_section(
1✔
1687
            lines_to_evaluate,
1✔
1688
            target_value,
1✔
1689
            assertion_context.headers,
1✔
1690
            assertion_context.trailers,
1✔
1691
            &context,
1✔
1692
            start_line,
1✔
1693
            assertion_context.timing,
1✔
1694
        );
1695

1696
        if !result.passed {
1✔
1697
            failure_reasons.extend(result.failure_messages);
×
1698
        }
1✔
1699
    }
1✔
1700

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

1733
    /// Log a response message for debug/verbose/raw modes.
1734
    fn log_response_message(msg: &Value, effective_no_assert: bool, verbose: bool) {
×
1735
        let should_format =
×
1736
            tracing::enabled!(tracing::Level::DEBUG) || effective_no_assert || verbose;
×
1737
        if !should_format {
×
1738
            return;
×
1739
        }
×
1740
        let pretty = runner_helpers::format_json_pretty(msg);
×
1741
        if tracing::enabled!(tracing::Level::DEBUG) {
×
1742
            tracing::debug!("Received Response:\n{}", pretty);
×
1743
        }
×
1744
        if effective_no_assert {
×
1745
            println!("--- RESPONSE (Raw) ---\n{}", pretty);
×
1746
        } else if verbose {
×
1747
            println!("🔍 gRPC response received: '{}'", pretty);
×
1748
        }
×
1749
    }
×
1750

1751
    /// Print dry-run preview of test execution
1752
    fn print_dry_run_preview(
1✔
1753
        &self,
1✔
1754
        document: &GctfDocument,
1✔
1755
        address: &str,
1✔
1756
        package: &str,
1✔
1757
        service: &str,
1✔
1758
        method: &str,
1✔
1759
    ) {
1✔
1760
        println!();
1✔
1761
        println!("🔍 Dry-Run Preview: {}", document.file_path);
1✔
1762
        println!("═══════════════════════════════════════════════════════════════");
1✔
1763
        println!();
1✔
1764
        println!("📍 Target:");
1✔
1765
        println!("   Address: {}", address);
1✔
1766
        let full_service = runner_helpers::full_service_name(package, service);
1✔
1767
        println!("   Endpoint: {} / {}", full_service, method);
1✔
1768
        println!();
1✔
1769

1770
        // Display headers first
1771
        let mut has_headers = false;
1✔
1772
        for section in &document.sections {
3✔
1773
            if section.section_type == SectionType::RequestHeaders {
3✔
1774
                if !has_headers {
×
1775
                    println!();
×
1776
                    println!("📋 Request Headers:");
×
1777
                    has_headers = true;
×
1778
                }
×
1779
                if let SectionContent::KeyValues(headers) = &section.content {
×
1780
                    for (key, value) in headers {
×
1781
                        println!("   {}: {}", key, value);
×
1782
                    }
×
1783
                }
×
1784
            }
3✔
1785
        }
1786

1787
        // Group requests and responses to show flow
1788
        let mut has_request = false;
1✔
1789
        let mut has_asserts = false;
1✔
1790
        let mut has_error = false;
1✔
1791

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

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

1912
        // Show PROTO config if present
1913
        if let Some(proto_config) = document.get_proto_config() {
1✔
1914
            println!();
×
1915
            println!("📄 Proto Configuration:");
×
1916
            if proto_config.contains_key("descriptor") {
×
1917
                println!("   Descriptor: {}", proto_config.get("descriptor").unwrap());
×
1918
            }
×
1919
            if proto_config.contains_key("files") {
×
1920
                println!("   Proto Files: {}", proto_config.get("files").unwrap());
×
1921
            }
×
1922
        }
1✔
1923

1924
        println!();
1✔
1925
        println!("═══════════════════════════════════════════════════════════════");
1✔
1926
        println!();
1✔
1927
    }
1✔
1928
}
1929

1930
#[cfg(test)]
1931
mod tests {
1932
    use super::*;
1933
    use crate::polyfill::runtime;
1934
    use serde_json::json;
1935
    use std::sync::Mutex;
1936

1937
    static ENV_MUTEX: Mutex<()> = Mutex::new(());
1938

1939
    #[test]
1940
    fn test_test_runner_new() {
1✔
1941
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
1942
        assert!(!runner.dry_run);
1✔
1943
        assert_eq!(runner.timeout_seconds, 30);
1✔
1944
        assert!(!runner.no_assert);
1✔
1945
        assert!(!runner.write_mode);
1✔
1946
        assert!(!runner.verbose);
1✔
1947
    }
1✔
1948

1949
    #[test]
1950
    fn test_test_runner_with_dry_run() {
1✔
1951
        let runner = TestRunner::new(true, 30, false, false, false, None);
1✔
1952
        assert!(runner.dry_run);
1✔
1953
    }
1✔
1954

1955
    #[test]
1956
    fn test_test_runner_with_timeout() {
1✔
1957
        let runner = TestRunner::new(false, 60, false, false, false, None);
1✔
1958
        assert_eq!(runner.timeout_seconds, 60);
1✔
1959
    }
1✔
1960

1961
    #[test]
1962
    fn test_test_runner_with_no_assert() {
1✔
1963
        let runner = TestRunner::new(false, 30, true, false, false, None);
1✔
1964
        assert!(runner.no_assert);
1✔
1965
    }
1✔
1966

1967
    #[test]
1968
    fn test_test_runner_with_write_mode() {
1✔
1969
        let runner = TestRunner::new(false, 30, false, true, false, None);
1✔
1970
        assert!(runner.write_mode);
1✔
1971
    }
1✔
1972

1973
    #[test]
1974
    fn test_parse_bool_flag_truthy_values() {
1✔
1975
        assert!(runner_helpers::parse_bool_flag("true"));
1✔
1976
        assert!(runner_helpers::parse_bool_flag("1"));
1✔
1977
        assert!(runner_helpers::parse_bool_flag("YES"));
1✔
1978
        assert!(runner_helpers::parse_bool_flag("on"));
1✔
1979
    }
1✔
1980

1981
    #[test]
1982
    fn test_parse_bool_flag_falsy_values() {
1✔
1983
        assert!(!runner_helpers::parse_bool_flag("false"));
1✔
1984
        assert!(!runner_helpers::parse_bool_flag("0"));
1✔
1985
        assert!(!runner_helpers::parse_bool_flag("off"));
1✔
1986
        assert!(!runner_helpers::parse_bool_flag(""));
1✔
1987
    }
1✔
1988

1989
    #[test]
1990
    fn test_parse_compression_option_from_options() {
1✔
1991
        let mut options = HashMap::new();
1✔
1992
        options.insert("compression".to_string(), "gzip".to_string());
1✔
1993

1994
        assert_eq!(
1✔
1995
            runner_helpers::parse_compression_option(&options),
1✔
1996
            crate::grpc::CompressionMode::Gzip
1997
        );
1998
    }
1✔
1999

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

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

2011
    #[test]
2012
    fn test_parse_compression_option_fallback_to_env() {
1✔
2013
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2014
        unsafe {
1✔
2015
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_COMPRESSION, "gzip");
1✔
2016
        }
1✔
2017

2018
        let mut options = HashMap::new();
1✔
2019
        options.insert("compression".to_string(), "invalid".to_string());
1✔
2020
        assert_eq!(
1✔
2021
            runner_helpers::parse_compression_option(&options),
1✔
2022
            crate::grpc::CompressionMode::Gzip
2023
        );
2024

2025
        unsafe {
1✔
2026
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_COMPRESSION);
1✔
2027
        }
1✔
2028
    }
1✔
2029

2030
    #[test]
2031
    fn test_resolve_tls_path_from_env_uses_cwd() {
1✔
2032
        if !runtime::supports(runtime::Capability::IsolatedFsIo) {
1✔
2033
            return;
×
2034
        }
1✔
2035

2036
        let cwd = std::env::current_dir().unwrap();
1✔
2037
        let document_path = Path::new("tests/fixtures/sample.gctf");
1✔
2038
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", true, document_path);
1✔
2039
        assert_eq!(Path::new(&resolved), cwd.join("certs/ca.crt"));
1✔
2040
    }
1✔
2041

2042
    #[test]
2043
    fn test_resolve_tls_path_from_env_without_fs_capability_returns_relative() {
1✔
2044
        if runtime::supports(runtime::Capability::IsolatedFsIo) {
1✔
2045
            return;
1✔
2046
        }
×
2047

2048
        let document_path = Path::new("tests/fixtures/sample.gctf");
×
NEW
2049
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", true, document_path);
×
2050
        assert_eq!(resolved, "certs/ca.crt");
×
2051
    }
1✔
2052

2053
    #[test]
2054
    fn test_resolve_tls_path_from_document_uses_document_dir() {
1✔
2055
        let document_path = Path::new("tests/fixtures/sample.gctf");
1✔
2056
        let resolved = runner_helpers::resolve_tls_path("certs/ca.crt", false, document_path);
1✔
2057
        assert_eq!(
1✔
2058
            Path::new(&resolved),
1✔
2059
            Path::new("tests/fixtures").join("certs").join("ca.crt")
1✔
2060
        );
2061
    }
1✔
2062

2063
    #[test]
2064
    fn test_tls_env_defaults_uses_grpctestify_prefix() {
1✔
2065
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2066

2067
        unsafe {
1✔
2068
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE, "/tmp/ca.pem");
1✔
2069
            std::env::set_var(
1✔
2070
                crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE,
1✔
2071
                "/tmp/cert.pem",
1✔
2072
            );
1✔
2073
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE, "/tmp/key.pem");
1✔
2074
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME, "localhost");
1✔
2075
        }
1✔
2076

2077
        let defaults = runner_helpers::tls_env_defaults();
1✔
2078
        assert_eq!(defaults.get("ca_cert"), Some(&"/tmp/ca.pem".to_string()));
1✔
2079
        assert_eq!(
1✔
2080
            defaults.get("client_cert"),
1✔
2081
            Some(&"/tmp/cert.pem".to_string())
1✔
2082
        );
2083
        assert_eq!(
1✔
2084
            defaults.get("client_key"),
1✔
2085
            Some(&"/tmp/key.pem".to_string())
1✔
2086
        );
2087
        assert_eq!(defaults.get("server_name"), Some(&"localhost".to_string()));
1✔
2088

2089
        unsafe {
1✔
2090
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE);
1✔
2091
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE);
1✔
2092
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE);
1✔
2093
            std::env::remove_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME);
1✔
2094
        }
1✔
2095
    }
1✔
2096

2097
    #[test]
2098
    fn test_tls_env_defaults_ignores_empty_values() {
1✔
2099
        let _guard = ENV_MUTEX.lock().unwrap();
1✔
2100

2101
        unsafe {
1✔
2102
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CA_FILE, "");
1✔
2103
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_CERT_FILE, "   ");
1✔
2104
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_KEY_FILE, "");
1✔
2105
            std::env::set_var(crate::config::ENV_GRPCTESTIFY_TLS_SERVER_NAME, " ");
1✔
2106
        }
1✔
2107

2108
        let defaults = runner_helpers::tls_env_defaults();
1✔
2109
        assert!(defaults.is_empty());
1✔
2110

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

2119
    #[test]
2120
    fn test_test_runner_with_verbose() {
1✔
2121
        let runner = TestRunner::new(false, 30, false, false, true, None);
1✔
2122
        assert!(runner.verbose);
1✔
2123
    }
1✔
2124

2125
    #[test]
2126
    fn test_grpc_code_name_from_numeric() {
1✔
2127
        assert_eq!(TestRunner::grpc_code_name_from_numeric(0), Some("OK"));
1✔
2128
        assert_eq!(TestRunner::grpc_code_name_from_numeric(5), Some("NotFound"));
1✔
2129
        assert_eq!(
1✔
2130
            TestRunner::grpc_code_name_from_numeric(13),
1✔
2131
            Some("Internal")
2132
        );
2133
        assert_eq!(TestRunner::grpc_code_name_from_numeric(99), None);
1✔
2134
    }
1✔
2135

2136
    #[test]
2137
    fn test_error_matches_expected_message() {
1✔
2138
        let expected = json!({
1✔
2139
            "message": "Can't find stub",
1✔
2140
            "code": 5
1✔
2141
        });
2142
        let error_text = "status: NotFound, message: \"Can't find stub\"";
1✔
2143
        assert!(TestRunner::error_matches_expected(error_text, &expected));
1✔
2144
    }
1✔
2145

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

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

2164
    #[test]
2165
    fn test_error_matches_expected_wrong_message() {
1✔
2166
        let expected = json!({
1✔
2167
            "message": "Different error"
1✔
2168
        });
2169
        let error_text = "status: NotFound, message: \"Can't find stub\"";
1✔
2170
        assert!(!TestRunner::error_matches_expected(error_text, &expected));
1✔
2171
    }
1✔
2172

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

2180
    #[test]
2181
    fn test_full_service_name() {
1✔
2182
        assert_eq!(
1✔
2183
            runner_helpers::full_service_name("package", "Service"),
1✔
2184
            "package.Service"
2185
        );
2186
        assert_eq!(runner_helpers::full_service_name("", "Service"), "Service");
1✔
2187
    }
1✔
2188

2189
    #[test]
2190
    fn test_substitute_variables_exact_match_preserves_type() {
1✔
2191
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2192
        let mut value = json!("{{ count }}");
1✔
2193
        let mut vars = HashMap::new();
1✔
2194
        vars.insert("count".to_string(), json!(42));
1✔
2195

2196
        runner.substitute_variables(&mut value, &vars);
1✔
2197
        assert_eq!(value, json!(42));
1✔
2198
    }
1✔
2199

2200
    #[test]
2201
    fn test_substitute_variables_interpolation_single_pass() {
1✔
2202
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2203
        let mut value = json!("id={{id}}, user={{ user }}, ok={{ok}}");
1✔
2204
        let mut vars = HashMap::new();
1✔
2205
        vars.insert("id".to_string(), json!(7));
1✔
2206
        vars.insert("user".to_string(), json!("alice"));
1✔
2207
        vars.insert("ok".to_string(), json!(true));
1✔
2208

2209
        runner.substitute_variables(&mut value, &vars);
1✔
2210
        assert_eq!(value, json!("id=7, user=alice, ok=true"));
1✔
2211
    }
1✔
2212

2213
    #[test]
2214
    fn test_substitute_variables_keeps_unknown_placeholder() {
1✔
2215
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2216
        let mut value = json!("hello {{known}} and {{unknown}}");
1✔
2217
        let mut vars = HashMap::new();
1✔
2218
        vars.insert("known".to_string(), json!("world"));
1✔
2219

2220
        runner.substitute_variables(&mut value, &vars);
1✔
2221
        assert_eq!(value, json!("hello world and {{unknown}}"));
1✔
2222
    }
1✔
2223

2224
    #[test]
2225
    fn test_expected_values_for_response_section() {
1✔
2226
        use crate::parser::ast::{InlineOptions, Section, SectionContent};
2227

2228
        let section = Section {
1✔
2229
            section_type: crate::parser::ast::SectionType::Response,
1✔
2230
            content: SectionContent::Json(json!({"key": "value"})),
1✔
2231
            inline_options: InlineOptions::default(),
1✔
2232
            raw_content: "".to_string(),
1✔
2233
            start_line: 1,
1✔
2234
            end_line: 2,
1✔
2235
        };
1✔
2236

2237
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2238
        assert_eq!(values.len(), 1);
1✔
2239
        assert_eq!(values[0], json!({"key": "value"}));
1✔
2240
    }
1✔
2241

2242
    #[test]
2243
    fn test_expected_values_for_json_lines() {
1✔
2244
        use crate::parser::ast::{InlineOptions, Section, SectionContent};
2245

2246
        let section = Section {
1✔
2247
            section_type: crate::parser::ast::SectionType::Response,
1✔
2248
            content: SectionContent::JsonLines(vec![
1✔
2249
                json!({"key1": "value1"}),
1✔
2250
                json!({"key2": "value2"}),
1✔
2251
            ]),
1✔
2252
            inline_options: InlineOptions::default(),
1✔
2253
            raw_content: "".to_string(),
1✔
2254
            start_line: 1,
1✔
2255
            end_line: 3,
1✔
2256
        };
1✔
2257

2258
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2259
        assert_eq!(values.len(), 2);
1✔
2260
    }
1✔
2261

2262
    #[test]
2263
    fn test_expected_values_for_other_section() {
1✔
2264
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2265

2266
        // The function returns values for any Json content, not just Response sections
2267
        // This is expected behavior - it extracts Json values regardless of section type
2268
        let section = Section {
1✔
2269
            section_type: SectionType::Request,
1✔
2270
            content: SectionContent::Json(json!({"key": "value"})),
1✔
2271
            inline_options: InlineOptions::default(),
1✔
2272
            raw_content: "".to_string(),
1✔
2273
            start_line: 1,
1✔
2274
            end_line: 2,
1✔
2275
        };
1✔
2276

2277
        let values = TestRunner::expected_values_for_response_section(&section);
1✔
2278
        // Returns 1 because the content is Json, even though it's a Request section
2279
        assert_eq!(values.len(), 1);
1✔
2280
        assert_eq!(values[0], json!({"key": "value"}));
1✔
2281
    }
1✔
2282

2283
    #[test]
2284
    fn test_metadata_map_to_hashmap_extracts_ascii_values() {
1✔
2285
        let mut metadata = tonic::metadata::MetadataMap::new();
1✔
2286
        metadata.insert("code", "EXTERNAL_SERVICE_ERROR_CODE".parse().unwrap());
1✔
2287
        metadata.insert("message", "External service error message".parse().unwrap());
1✔
2288

2289
        let trailers = runner_helpers::metadata_map_to_hashmap(&metadata);
1✔
2290
        assert_eq!(
1✔
2291
            trailers.get("code"),
1✔
2292
            Some(&"EXTERNAL_SERVICE_ERROR_CODE".to_string())
1✔
2293
        );
2294
        assert_eq!(
1✔
2295
            trailers.get("message"),
1✔
2296
            Some(&"External service error message".to_string())
1✔
2297
        );
2298
    }
1✔
2299

2300
    #[test]
2301
    fn test_assertion_scope_timing_single_message_scope() {
1✔
2302
        let mut timing = AssertionScopeTimingState::default();
1✔
2303

2304
        let first = timing.finish_scope(0, 12, 1).unwrap();
1✔
2305

2306
        assert_eq!(first.elapsed_ms, 12);
1✔
2307
        assert_eq!(first.total_elapsed_ms, 12);
1✔
2308
        assert_eq!(first.scope_message_count, 1);
1✔
2309
        assert_eq!(first.scope_index, 1);
1✔
2310
    }
1✔
2311

2312
    #[test]
2313
    fn test_assertion_scope_timing_batch_scope_uses_full_section_window() {
1✔
2314
        let mut timing = AssertionScopeTimingState::default();
1✔
2315

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

2318
        assert_eq!(batch.elapsed_ms, 27);
1✔
2319
        assert_eq!(batch.total_elapsed_ms, 27);
1✔
2320
        assert_eq!(batch.scope_message_count, 2);
1✔
2321
        assert_eq!(batch.scope_index, 1);
1✔
2322
    }
1✔
2323

2324
    #[test]
2325
    fn test_assertion_scope_timing_accumulates_total_duration() {
1✔
2326
        let mut timing = AssertionScopeTimingState::default();
1✔
2327

2328
        let first = timing.finish_scope(0, 10, 1).unwrap();
1✔
2329
        let second = timing.finish_scope(10, 35, 3).unwrap();
1✔
2330

2331
        assert_eq!(first.elapsed_ms, 10);
1✔
2332
        assert_eq!(first.total_elapsed_ms, 10);
1✔
2333
        assert_eq!(second.elapsed_ms, 25);
1✔
2334
        assert_eq!(second.total_elapsed_ms, 35);
1✔
2335
        assert_eq!(second.scope_message_count, 3);
1✔
2336
        assert_eq!(second.scope_index, 2);
1✔
2337
    }
1✔
2338

2339
    #[test]
2340
    fn test_has_required_followup_asserts_for_error_requires_adjacent_asserts() {
1✔
2341
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2342

2343
        let error = Section {
1✔
2344
            section_type: SectionType::Error,
1✔
2345
            content: SectionContent::Empty,
1✔
2346
            inline_options: InlineOptions {
1✔
2347
                with_asserts: true,
1✔
2348
                ..InlineOptions::default()
1✔
2349
            },
1✔
2350
            raw_content: "".to_string(),
1✔
2351
            start_line: 12,
1✔
2352
            end_line: 12,
1✔
2353
        };
1✔
2354
        let sections = vec![error.clone()];
1✔
2355
        let mut failures = Vec::new();
1✔
2356

2357
        let has_followup =
1✔
2358
            TestRunner::has_required_followup_asserts(&error, &sections, 0, false, &mut failures);
1✔
2359

2360
        assert!(!has_followup);
1✔
2361
        assert_eq!(failures.len(), 1);
1✔
2362
        assert!(failures[0].contains("ERROR at line 12 has 'with_asserts'"));
1✔
2363
    }
1✔
2364

2365
    #[test]
2366
    fn test_has_required_followup_asserts_for_error_accepts_adjacent_asserts() {
1✔
2367
        use crate::parser::ast::{InlineOptions, Section, SectionContent, SectionType};
2368

2369
        let error = Section {
1✔
2370
            section_type: SectionType::Error,
1✔
2371
            content: SectionContent::Empty,
1✔
2372
            inline_options: InlineOptions {
1✔
2373
                with_asserts: true,
1✔
2374
                ..InlineOptions::default()
1✔
2375
            },
1✔
2376
            raw_content: "".to_string(),
1✔
2377
            start_line: 20,
1✔
2378
            end_line: 20,
1✔
2379
        };
1✔
2380
        let asserts = Section {
1✔
2381
            section_type: SectionType::Asserts,
1✔
2382
            content: SectionContent::Assertions(vec![".code == 5".to_string()]),
1✔
2383
            inline_options: InlineOptions::default(),
1✔
2384
            raw_content: ".code == 5".to_string(),
1✔
2385
            start_line: 21,
1✔
2386
            end_line: 21,
1✔
2387
        };
1✔
2388
        let sections = vec![error.clone(), asserts];
1✔
2389
        let mut failures = Vec::new();
1✔
2390

2391
        let has_followup =
1✔
2392
            TestRunner::has_required_followup_asserts(&error, &sections, 0, false, &mut failures);
1✔
2393

2394
        assert!(has_followup);
1✔
2395
        assert!(failures.is_empty());
1✔
2396
    }
1✔
2397

2398
    #[test]
2399
    fn test_error_assertions_evaluate_against_error_json_object() {
1✔
2400
        let runner = TestRunner::new(false, 30, false, false, false, None);
1✔
2401
        let target = json!({
1✔
2402
            "code": 5,
1✔
2403
            "message": "resource not found in backend",
1✔
2404
            "details": [
1✔
2405
                {
2406
                    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
1✔
2407
                    "reason": "NOT_FOUND"
1✔
2408
                }
2409
            ]
2410
        });
2411
        let lines = vec![
1✔
2412
            ".code == 5".to_string(),
1✔
2413
            ".message contains \"not found\"".to_string(),
1✔
2414
            ".details[0][\"@type\"] == \"type.googleapis.com/google.rpc.ErrorInfo\"".to_string(),
1✔
2415
        ];
2416
        let mut failures = Vec::new();
1✔
2417
        let headers: HashMap<String, String> = HashMap::new();
1✔
2418
        let trailers: HashMap<String, String> = HashMap::new();
1✔
2419

2420
        runner.run_assertions(
1✔
2421
            &lines,
1✔
2422
            &target,
1✔
2423
            &mut failures,
1✔
2424
            "(attached to ERROR at line 1)".to_string(),
1✔
2425
            1,
2426
            AssertionContext {
1✔
2427
                headers: &headers,
1✔
2428
                trailers: &trailers,
1✔
2429
                timing: None,
1✔
2430
            },
1✔
2431
        );
2432

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