Skip to main content

asupersync_conformance/
h2_ping_conformance.rs

1//! HTTP/2 PING frame conformance testing.
2//!
3//! This harness exercises the asupersync HTTP/2 connection's PING frame
4//! handling against RFC-backed expected states. The h2 reference side is not
5//! wired yet, so matching the local expected state is reported as XFAIL instead
6//! of a vendor-parity pass.
7
8use asupersync::http::h2::{
9    Connection, Settings,
10    frame::{Frame, PingFrame, SettingsFrame},
11};
12#[cfg(test)]
13use asupersync::{
14    bytes::Bytes,
15    http::h2::frame::{FrameHeader, FrameType, parse_frame},
16};
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20const H2_REFERENCE_UNAVAILABLE: &str =
21    "h2 reference comparison unavailable in standalone frame harness";
22
23/// Test verdict for individual conformance cases.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum PingTestVerdict {
26    Pass,
27    Fail,
28    ExpectedFailure, // Known divergence
29    Skipped,
30}
31
32impl fmt::Display for PingTestVerdict {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Pass => write!(f, "PASS"),
36            Self::Fail => write!(f, "FAIL"),
37            Self::ExpectedFailure => write!(f, "XFAIL"),
38            Self::Skipped => write!(f, "SKIP"),
39        }
40    }
41}
42
43/// Requirement level for conformance testing.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub enum RequirementLevel {
46    Must,   // RFC MUST
47    Should, // RFC SHOULD
48    May,    // RFC MAY
49}
50
51/// PING operation timing for RTT calculation.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct PingTiming {
54    /// When the PING was sent
55    pub sent_at_ms: u64,
56    /// When the PING_ACK was received
57    pub ack_received_at_ms: Option<u64>,
58    /// Computed RTT in milliseconds
59    pub rtt_ms: Option<u64>,
60}
61
62/// Connection state after PING processing for comparison.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct PingConnectionState {
65    /// Connection state (should remain stable - no spurious GOAWAY)
66    pub connection_state: String,
67    /// Number of pending operations (PING ACKs to send)
68    pub pending_ping_acks: usize,
69    /// RTT measurements collected
70    pub ping_timings: Vec<PingTiming>,
71    /// Whether any spurious errors occurred
72    pub has_errors: bool,
73}
74
75/// Serializable PING frame for test cases.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SerializablePingFrame {
78    pub opaque_data: [u8; 8],
79    pub ack: bool,
80    /// Deterministic timestamp for RTT calculation (milliseconds)
81    pub timestamp_ms: u64,
82}
83
84impl From<PingFrame> for SerializablePingFrame {
85    fn from(frame: PingFrame) -> Self {
86        Self {
87            opaque_data: frame.opaque_data,
88            ack: frame.ack,
89            timestamp_ms: 0, // Will be set during test execution
90        }
91    }
92}
93
94impl From<SerializablePingFrame> for PingFrame {
95    fn from(frame: SerializablePingFrame) -> Self {
96        Self {
97            opaque_data: frame.opaque_data,
98            ack: frame.ack,
99        }
100    }
101}
102
103/// Single conformance test case for PING frame handling.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PingConformanceCase {
106    pub id: String,
107    pub description: String,
108    pub requirement_level: RequirementLevel,
109    /// Sequence of PING frames to apply (includes PING and PING_ACK)
110    pub ping_sequence: Vec<SerializablePingFrame>,
111    /// Expected final connection state
112    pub expected_connection_state: PingConnectionState,
113    /// Expected RTT behavior (within tolerance)
114    pub expected_rtt_behavior: String,
115}
116
117/// Result of a single conformance test.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct PingConformanceResult {
120    pub case_id: String,
121    pub verdict: PingTestVerdict,
122    pub error: Option<String>,
123    /// Asupersync's final connection state
124    pub asupersync_state: Option<PingConnectionState>,
125    /// H2 reference's final connection state
126    pub h2_state: Option<PingConnectionState>,
127    /// Differences detected between implementations
128    pub differences: Vec<String>,
129}
130
131/// Summary statistics for the conformance run.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct PingComplianceSummary {
134    pub total_cases: usize,
135    pub passed: usize,
136    pub failed: usize,
137    pub expected_failures: usize,
138    pub skipped: usize,
139    pub compliance_score: f64, // passed / total
140}
141
142/// Complete conformance test report.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct PingComplianceReport {
145    pub test_run_id: String,
146    pub timestamp: chrono::DateTime<chrono::Utc>,
147    pub total_cases: usize,
148    pub results: Vec<PingConformanceResult>,
149    pub summary: PingComplianceSummary,
150}
151
152impl PingComplianceReport {
153    /// Create a new report with generated ID and timestamp.
154    fn new(results: Vec<PingConformanceResult>) -> Self {
155        let total_cases = results.len();
156        let passed = results
157            .iter()
158            .filter(|r| r.verdict == PingTestVerdict::Pass)
159            .count();
160        let failed = results
161            .iter()
162            .filter(|r| r.verdict == PingTestVerdict::Fail)
163            .count();
164        let expected_failures = results
165            .iter()
166            .filter(|r| r.verdict == PingTestVerdict::ExpectedFailure)
167            .count();
168        let skipped = results
169            .iter()
170            .filter(|r| r.verdict == PingTestVerdict::Skipped)
171            .count();
172
173        let compliance_score = if total_cases > 0 {
174            passed as f64 / total_cases as f64
175        } else {
176            1.0
177        };
178
179        let summary = PingComplianceSummary {
180            total_cases,
181            passed,
182            failed,
183            expected_failures,
184            skipped,
185            compliance_score,
186        };
187
188        Self {
189            test_run_id: uuid::Uuid::new_v4().to_string(),
190            timestamp: chrono::Utc::now(),
191            total_cases,
192            results,
193            summary,
194        }
195    }
196}
197
198/// Main conformance tester for HTTP/2 PING frames.
199#[derive(Debug)]
200pub struct PingConformanceTester {
201    pub test_cases: Vec<PingConformanceCase>,
202}
203
204impl PingConformanceTester {
205    /// Create a new tester with predefined conformance cases.
206    pub fn new() -> Self {
207        Self {
208            test_cases: create_ping_test_cases(),
209        }
210    }
211
212    /// Run all conformance tests and return a report.
213    pub async fn run_all_tests(&self) -> PingComplianceReport {
214        let mut results = Vec::new();
215
216        for case in &self.test_cases {
217            let result = self.run_single_test(case).await;
218            results.push(result);
219        }
220
221        PingComplianceReport::new(results)
222    }
223
224    /// Run a single conformance test case.
225    async fn run_single_test(&self, case: &PingConformanceCase) -> PingConformanceResult {
226        // Test asupersync implementation
227        let asupersync_result = self.test_asupersync_ping(case).await;
228
229        // Test h2 reference implementation. If the external reference is not wired,
230        // keep this as a live conformance check against the RFC-backed expected
231        // state in the test case instead of reporting an invented comparison.
232        let h2_result = self.test_h2_ping(case).await;
233
234        // Compare results
235        let (verdict, error, differences) = match (&asupersync_result, &h2_result) {
236            (Ok(asupersync_state), Err(h2_err)) if h2_err == H2_REFERENCE_UNAVAILABLE => {
237                let differences = self
238                    .compare_connection_states(asupersync_state, &case.expected_connection_state);
239                if differences.is_empty() {
240                    (
241                        PingTestVerdict::ExpectedFailure,
242                        Some(format!(
243                            "{h2_err}; live asupersync matched the RFC-expected state but vendor parity remains unexercised"
244                        )),
245                        differences,
246                    )
247                } else {
248                    (
249                        PingTestVerdict::Fail,
250                        Some(format!(
251                            "Live asupersync state differed from expected RFC behavior while {h2_err}"
252                        )),
253                        differences,
254                    )
255                }
256            }
257            (Err(asupersync_err), Err(h2_err)) if h2_err == H2_REFERENCE_UNAVAILABLE => (
258                PingTestVerdict::Fail,
259                Some(format!(
260                    "Live asupersync PING processing failed while {h2_err}: {asupersync_err}"
261                )),
262                vec![format!("asupersync_error: {asupersync_err}")],
263            ),
264            (_, Err(h2_err)) if h2_err == H2_REFERENCE_UNAVAILABLE => (
265                PingTestVerdict::Skipped,
266                Some(H2_REFERENCE_UNAVAILABLE.to_string()),
267                Vec::new(),
268            ),
269            (Ok(asupersync_state), Ok(h2_state)) => {
270                let differences = self.compare_connection_states(asupersync_state, h2_state);
271                if differences.is_empty() {
272                    (PingTestVerdict::Pass, None, differences)
273                } else {
274                    (
275                        PingTestVerdict::Fail,
276                        Some(format!(
277                            "Connection state differences: {}",
278                            differences.join(", ")
279                        )),
280                        differences,
281                    )
282                }
283            }
284            (Err(asupersync_err), Err(h2_err)) => {
285                // Both failed - check if they failed the same way
286                if asupersync_err == h2_err {
287                    (PingTestVerdict::Pass, None, Vec::new())
288                } else {
289                    (
290                        PingTestVerdict::Fail,
291                        Some(format!(
292                            "Different error behaviors: asupersync={}, h2={}",
293                            asupersync_err, h2_err
294                        )),
295                        vec![format!(
296                            "Error divergence: {} vs {}",
297                            asupersync_err, h2_err
298                        )],
299                    )
300                }
301            }
302            (Ok(_), Err(h2_err)) => (
303                PingTestVerdict::Fail,
304                Some(format!("asupersync succeeded, h2 failed: {}", h2_err)),
305                vec!["Implementation success divergence".to_string()],
306            ),
307            (Err(asupersync_err), Ok(_)) => (
308                PingTestVerdict::Fail,
309                Some(format!(
310                    "asupersync failed, h2 succeeded: {}",
311                    asupersync_err
312                )),
313                vec!["Implementation success divergence".to_string()],
314            ),
315        };
316
317        PingConformanceResult {
318            case_id: case.id.clone(),
319            verdict,
320            error,
321            asupersync_state: asupersync_result.as_ref().ok().cloned(),
322            h2_state: h2_result.as_ref().ok().cloned(),
323            differences,
324        }
325    }
326
327    /// Test asupersync PING handling.
328    async fn test_asupersync_ping(
329        &self,
330        case: &PingConformanceCase,
331    ) -> Result<PingConnectionState, String> {
332        let settings = Settings::default();
333        let mut connection = Connection::server(settings);
334        let mut ping_timings = Vec::new();
335        let mut outstanding_ping_timings: Vec<([u8; 8], usize)> = Vec::new();
336        accept_peer_settings(&mut connection)?;
337
338        // Apply PING sequence with timing
339        for serializable_frame in &case.ping_sequence {
340            let ping_frame: PingFrame = serializable_frame.clone().into();
341
342            if !ping_frame.ack {
343                // This is a PING request - track timing
344                let timing = PingTiming {
345                    sent_at_ms: serializable_frame.timestamp_ms,
346                    ack_received_at_ms: None,
347                    rtt_ms: None,
348                };
349                ping_timings.push(timing);
350                outstanding_ping_timings.push((ping_frame.opaque_data, ping_timings.len() - 1));
351            } else {
352                // This is a PING ACK - update timing
353                if let Some(position) =
354                    outstanding_ping_timings
355                        .iter()
356                        .position(|(opaque_data, index)| {
357                            *opaque_data == ping_frame.opaque_data
358                                && ping_timings[*index].ack_received_at_ms.is_none()
359                        })
360                {
361                    let (_, timing_index) = outstanding_ping_timings.remove(position);
362                    let timing = &mut ping_timings[timing_index];
363                    timing.ack_received_at_ms = Some(serializable_frame.timestamp_ms);
364                    timing.rtt_ms = Some(
365                        serializable_frame
366                            .timestamp_ms
367                            .saturating_sub(timing.sent_at_ms),
368                    );
369                }
370            }
371
372            // Process the PING frame
373            if let Err(e) = process_live_ping_frame(&mut connection, &ping_frame) {
374                return Err(format!("Failed to process PING frame: {}", e));
375            }
376        }
377
378        // Extract connection state
379        let connection_state = extract_asupersync_ping_state(&mut connection, ping_timings)?;
380        Ok(connection_state)
381    }
382
383    /// Test h2 reference PING handling.
384    async fn test_h2_ping(
385        &self,
386        _case: &PingConformanceCase,
387    ) -> Result<PingConnectionState, String> {
388        Err(H2_REFERENCE_UNAVAILABLE.to_string())
389    }
390
391    /// Compare connection states between implementations.
392    fn compare_connection_states(
393        &self,
394        asupersync: &PingConnectionState,
395        h2: &PingConnectionState,
396    ) -> Vec<String> {
397        let mut differences = Vec::new();
398
399        if asupersync.connection_state != h2.connection_state {
400            differences.push(format!(
401                "connection_state differs: asupersync={}, h2={}",
402                asupersync.connection_state, h2.connection_state
403            ));
404        }
405
406        if asupersync.pending_ping_acks != h2.pending_ping_acks {
407            differences.push(format!(
408                "pending_ping_acks differs: asupersync={}, h2={}",
409                asupersync.pending_ping_acks, h2.pending_ping_acks
410            ));
411        }
412
413        if asupersync.has_errors != h2.has_errors {
414            differences.push(format!(
415                "has_errors differs: asupersync={}, h2={}",
416                asupersync.has_errors, h2.has_errors
417            ));
418        }
419
420        // Compare ping timings length
421        if asupersync.ping_timings.len() != h2.ping_timings.len() {
422            differences.push(format!(
423                "ping_timings count differs: asupersync={}, h2={}",
424                asupersync.ping_timings.len(),
425                h2.ping_timings.len()
426            ));
427        } else {
428            // Compare RTT values (within tolerance)
429            for (i, (asupersync_timing, h2_timing)) in asupersync
430                .ping_timings
431                .iter()
432                .zip(&h2.ping_timings)
433                .enumerate()
434            {
435                if let (Some(asupersync_rtt), Some(h2_rtt)) =
436                    (asupersync_timing.rtt_ms, h2_timing.rtt_ms)
437                {
438                    let diff = asupersync_rtt.abs_diff(h2_rtt);
439                    if diff > 5 {
440                        // 5ms tolerance
441                        differences.push(format!(
442                            "ping_timing[{}] RTT differs by {}ms: asupersync={}ms, h2={}ms",
443                            i, diff, asupersync_rtt, h2_rtt
444                        ));
445                    }
446                } else if asupersync_timing.rtt_ms != h2_timing.rtt_ms {
447                    differences.push(format!(
448                        "ping_timing[{}] RTT availability differs: asupersync={:?}ms, h2={:?}ms",
449                        i, asupersync_timing.rtt_ms, h2_timing.rtt_ms
450                    ));
451                }
452            }
453        }
454
455        differences
456    }
457
458    /// Generate a markdown report.
459    pub fn generate_markdown_report(&self, report: &PingComplianceReport) -> String {
460        let mut output = String::new();
461        output.push_str("# HTTP/2 PING Frame Conformance Report\n\n");
462
463        output.push_str(&format!("**Test Run ID:** {}\n", report.test_run_id));
464        output.push_str(&format!("**Timestamp:** {}\n", report.timestamp));
465        output.push_str(&format!("**Total Test Cases:** {}\n\n", report.total_cases));
466
467        output.push_str("## Summary\n\n");
468        output.push_str(&format!("- **Passed:** {}\n", report.summary.passed));
469        output.push_str(&format!("- **Failed:** {}\n", report.summary.failed));
470        output.push_str(&format!(
471            "- **Expected Failures:** {}\n",
472            report.summary.expected_failures
473        ));
474        output.push_str(&format!("- **Skipped:** {}\n", report.summary.skipped));
475        output.push_str(&format!(
476            "- **Compliance Score:** {:.1}%\n\n",
477            report.summary.compliance_score * 100.0
478        ));
479
480        if report.summary.failed > 0 {
481            output.push_str("## Failures\n\n");
482            for result in &report.results {
483                if result.verdict == PingTestVerdict::Fail {
484                    output.push_str(&format!("### {}\n", result.case_id));
485                    if let Some(error) = &result.error {
486                        output.push_str(&format!("**Error:** {}\n", error));
487                    }
488                    if !result.differences.is_empty() {
489                        output.push_str("**Differences:**\n");
490                        for diff in &result.differences {
491                            output.push_str(&format!("- {}\n", diff));
492                        }
493                    }
494                    output.push('\n');
495                }
496            }
497        }
498
499        output.push_str("## All Results\n\n");
500        output.push_str("| Case ID | Verdict | Description |\n");
501        output.push_str("|---------|---------|-------------|\n");
502        for result in &report.results {
503            output.push_str(&format!(
504                "| {} | {} | Case {} |\n",
505                result.case_id, result.verdict, result.case_id
506            ));
507        }
508
509        output
510    }
511}
512
513impl Default for PingConformanceTester {
514    fn default() -> Self {
515        Self::new()
516    }
517}
518
519fn accept_peer_settings(connection: &mut Connection) -> Result<(), String> {
520    let received = connection
521        .process_frame(Frame::Settings(SettingsFrame::new(vec![])))
522        .map_err(|err| err.to_string())?;
523    if received.is_some() {
524        return Err("SETTINGS handshake produced an application frame".to_string());
525    }
526
527    match connection.next_frame() {
528        Some(Frame::Settings(settings)) if settings.ack => Ok(()),
529        other => Err(format!(
530            "SETTINGS handshake should queue exactly one ACK, got {other:?}"
531        )),
532    }
533}
534
535/// Process a PING frame through the production connection state machine.
536fn process_live_ping_frame(
537    connection: &mut Connection,
538    ping_frame: &PingFrame,
539) -> Result<(), String> {
540    let received = connection
541        .process_frame(Frame::Ping(*ping_frame))
542        .map_err(|err| err.to_string())?;
543    if received.is_some() {
544        return Err(format!(
545            "PING produced unexpected application frame: {received:?}"
546        ));
547    }
548    Ok(())
549}
550
551/// Extract PING-related connection state from asupersync connection.
552fn extract_asupersync_ping_state(
553    connection: &mut Connection,
554    ping_timings: Vec<PingTiming>,
555) -> Result<PingConnectionState, String> {
556    let mut pending_ping_acks = 0;
557    while connection.has_pending_frames() {
558        match connection.next_frame() {
559            Some(Frame::Ping(ping)) if ping.ack => pending_ping_acks += 1,
560            Some(frame) => {
561                return Err(format!(
562                    "unexpected pending frame after PING processing: {frame:?}"
563                ));
564            }
565            None => break,
566        }
567    }
568
569    Ok(PingConnectionState {
570        connection_state: format!("{:?}", connection.state()),
571        pending_ping_acks,
572        ping_timings,
573        has_errors: false,
574    })
575}
576
577/// Create predefined test cases for PING frame conformance.
578fn create_ping_test_cases() -> Vec<PingConformanceCase> {
579    vec![
580        // Test Case 1: Basic PING/PING_ACK exchange
581        PingConformanceCase {
582            id: "ping-001".to_string(),
583            description: "Basic PING frame generates PING_ACK response".to_string(),
584            requirement_level: RequirementLevel::Must,
585            ping_sequence: vec![
586                SerializablePingFrame {
587                    opaque_data: [1, 2, 3, 4, 5, 6, 7, 8],
588                    ack: false,
589                    timestamp_ms: 0,
590                },
591                SerializablePingFrame {
592                    opaque_data: [1, 2, 3, 4, 5, 6, 7, 8],
593                    ack: true,
594                    timestamp_ms: 50, // 50ms later
595                },
596            ],
597            expected_connection_state: PingConnectionState {
598                connection_state: "Open".to_string(),
599                pending_ping_acks: 1,
600                ping_timings: vec![PingTiming {
601                    sent_at_ms: 0,
602                    ack_received_at_ms: Some(50),
603                    rtt_ms: Some(50),
604                }],
605                has_errors: false,
606            },
607            expected_rtt_behavior: "RTT calculated from PING/ACK timing".to_string(),
608        },
609        // Test Case 2: PING with zero payload
610        PingConformanceCase {
611            id: "ping-002".to_string(),
612            description: "PING with zero payload works correctly".to_string(),
613            requirement_level: RequirementLevel::Must,
614            ping_sequence: vec![
615                SerializablePingFrame {
616                    opaque_data: [0, 0, 0, 0, 0, 0, 0, 0],
617                    ack: false,
618                    timestamp_ms: 0,
619                },
620                SerializablePingFrame {
621                    opaque_data: [0, 0, 0, 0, 0, 0, 0, 0],
622                    ack: true,
623                    timestamp_ms: 25,
624                },
625            ],
626            expected_connection_state: PingConnectionState {
627                connection_state: "Open".to_string(),
628                pending_ping_acks: 1,
629                ping_timings: vec![PingTiming {
630                    sent_at_ms: 0,
631                    ack_received_at_ms: Some(25),
632                    rtt_ms: Some(25),
633                }],
634                has_errors: false,
635            },
636            expected_rtt_behavior: "RTT calculated correctly with zero payload".to_string(),
637        },
638        // Test Case 3: PING with maximum payload
639        PingConformanceCase {
640            id: "ping-003".to_string(),
641            description: "PING with maximum payload (0xFF bytes)".to_string(),
642            requirement_level: RequirementLevel::Must,
643            ping_sequence: vec![
644                SerializablePingFrame {
645                    opaque_data: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
646                    ack: false,
647                    timestamp_ms: 100,
648                },
649                SerializablePingFrame {
650                    opaque_data: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
651                    ack: true,
652                    timestamp_ms: 175,
653                },
654            ],
655            expected_connection_state: PingConnectionState {
656                connection_state: "Open".to_string(),
657                pending_ping_acks: 1,
658                ping_timings: vec![PingTiming {
659                    sent_at_ms: 100,
660                    ack_received_at_ms: Some(175),
661                    rtt_ms: Some(75),
662                }],
663                has_errors: false,
664            },
665            expected_rtt_behavior: "RTT calculated correctly with max payload".to_string(),
666        },
667        // Test Case 4: Multiple PING exchanges
668        PingConformanceCase {
669            id: "ping-004".to_string(),
670            description: "Multiple PING/PING_ACK exchanges track RTT correctly".to_string(),
671            requirement_level: RequirementLevel::Should,
672            ping_sequence: vec![
673                // First PING
674                SerializablePingFrame {
675                    opaque_data: [1, 0, 0, 0, 0, 0, 0, 0],
676                    ack: false,
677                    timestamp_ms: 0,
678                },
679                SerializablePingFrame {
680                    opaque_data: [1, 0, 0, 0, 0, 0, 0, 0],
681                    ack: true,
682                    timestamp_ms: 30,
683                },
684                // Second PING
685                SerializablePingFrame {
686                    opaque_data: [2, 0, 0, 0, 0, 0, 0, 0],
687                    ack: false,
688                    timestamp_ms: 100,
689                },
690                SerializablePingFrame {
691                    opaque_data: [2, 0, 0, 0, 0, 0, 0, 0],
692                    ack: true,
693                    timestamp_ms: 140,
694                },
695            ],
696            expected_connection_state: PingConnectionState {
697                connection_state: "Open".to_string(),
698                pending_ping_acks: 2,
699                ping_timings: vec![
700                    PingTiming {
701                        sent_at_ms: 0,
702                        ack_received_at_ms: Some(30),
703                        rtt_ms: Some(30),
704                    },
705                    PingTiming {
706                        sent_at_ms: 100,
707                        ack_received_at_ms: Some(140),
708                        rtt_ms: Some(40),
709                    },
710                ],
711                has_errors: false,
712            },
713            expected_rtt_behavior: "Multiple RTT measurements maintained".to_string(),
714        },
715        // Test Case 5: PING without ACK (pending state)
716        PingConformanceCase {
717            id: "ping-005".to_string(),
718            description: "PING without matching ACK remains pending".to_string(),
719            requirement_level: RequirementLevel::Must,
720            ping_sequence: vec![
721                SerializablePingFrame {
722                    opaque_data: [9, 8, 7, 6, 5, 4, 3, 2],
723                    ack: false,
724                    timestamp_ms: 0,
725                },
726                // No corresponding PING_ACK
727            ],
728            expected_connection_state: PingConnectionState {
729                connection_state: "Open".to_string(),
730                pending_ping_acks: 1, // Should be pending
731                ping_timings: vec![PingTiming {
732                    sent_at_ms: 0,
733                    ack_received_at_ms: None,
734                    rtt_ms: None,
735                }],
736                has_errors: false,
737            },
738            expected_rtt_behavior: "Pending PING tracked without RTT".to_string(),
739        },
740        // Test Case 6: PING ACK only (no corresponding PING)
741        PingConformanceCase {
742            id: "ping-006".to_string(),
743            description: "Received PING_ACK without PING should not cause errors".to_string(),
744            requirement_level: RequirementLevel::Should,
745            ping_sequence: vec![SerializablePingFrame {
746                opaque_data: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22],
747                ack: true, // ACK without corresponding PING
748                timestamp_ms: 50,
749            }],
750            expected_connection_state: PingConnectionState {
751                connection_state: "Open".to_string(),
752                pending_ping_acks: 0,
753                ping_timings: Vec::new(),
754                has_errors: false, // Should not cause connection errors
755            },
756            expected_rtt_behavior: "Orphan PING_ACK ignored gracefully".to_string(),
757        },
758        // Test Case 7: High-frequency PING stress test
759        PingConformanceCase {
760            id: "ping-007".to_string(),
761            description: "High-frequency PING exchanges maintain stability".to_string(),
762            requirement_level: RequirementLevel::May,
763            ping_sequence: vec![
764                // Rapid succession of PINGs
765                SerializablePingFrame {
766                    opaque_data: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
767                    ack: false,
768                    timestamp_ms: 0,
769                },
770                SerializablePingFrame {
771                    opaque_data: [0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02],
772                    ack: false,
773                    timestamp_ms: 5,
774                },
775                SerializablePingFrame {
776                    opaque_data: [0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03],
777                    ack: false,
778                    timestamp_ms: 10,
779                },
780                // Corresponding ACKs
781                SerializablePingFrame {
782                    opaque_data: [0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
783                    ack: true,
784                    timestamp_ms: 15,
785                },
786                SerializablePingFrame {
787                    opaque_data: [0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02],
788                    ack: true,
789                    timestamp_ms: 20,
790                },
791                SerializablePingFrame {
792                    opaque_data: [0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03],
793                    ack: true,
794                    timestamp_ms: 25,
795                },
796            ],
797            expected_connection_state: PingConnectionState {
798                connection_state: "Open".to_string(), // No spurious GOAWAY
799                pending_ping_acks: 3,
800                ping_timings: vec![
801                    PingTiming {
802                        sent_at_ms: 0,
803                        ack_received_at_ms: Some(15),
804                        rtt_ms: Some(15),
805                    },
806                    PingTiming {
807                        sent_at_ms: 5,
808                        ack_received_at_ms: Some(20),
809                        rtt_ms: Some(15),
810                    },
811                    PingTiming {
812                        sent_at_ms: 10,
813                        ack_received_at_ms: Some(25),
814                        rtt_ms: Some(15),
815                    },
816                ],
817                has_errors: false,
818            },
819            expected_rtt_behavior: "High-frequency PING does not destabilize connection"
820                .to_string(),
821        },
822    ]
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828
829    #[test]
830    fn non_ack_ping_queues_one_ack_with_same_opaque_data() {
831        let mut connection = Connection::server(Settings::default());
832        accept_peer_settings(&mut connection).expect("SETTINGS handshake");
833
834        let ping = PingFrame::new(*b"pingpong");
835        process_live_ping_frame(&mut connection, &ping).expect("PING should process");
836
837        match connection.next_frame() {
838            Some(Frame::Ping(ack)) => {
839                assert!(ack.ack);
840                assert_eq!(ack.opaque_data, *b"pingpong");
841            }
842            other => panic!("expected PING ACK, got {other:?}"),
843        }
844        assert!(!connection.has_pending_frames());
845    }
846
847    #[test]
848    fn ping_ack_does_not_queue_another_ack() {
849        let mut connection = Connection::server(Settings::default());
850        accept_peer_settings(&mut connection).expect("SETTINGS handshake");
851
852        let ping_ack = PingFrame::ack(*b"ack-only");
853        process_live_ping_frame(&mut connection, &ping_ack).expect("PING ACK should process");
854
855        assert!(
856            !connection.has_pending_frames(),
857            "incoming PING ACK must not be ACKed again"
858        );
859    }
860
861    #[test]
862    fn invalid_ping_payload_length_is_rejected_by_parser() {
863        let short_header = FrameHeader {
864            length: 7,
865            frame_type: FrameType::Ping as u8,
866            flags: 0,
867            stream_id: 0,
868        };
869        assert!(
870            parse_frame(&short_header, Bytes::from_static(b"1234567")).is_err(),
871            "PING payloads shorter than 8 bytes must be rejected"
872        );
873
874        let long_header = FrameHeader {
875            length: 9,
876            frame_type: FrameType::Ping as u8,
877            flags: 0,
878            stream_id: 0,
879        };
880        assert!(
881            parse_frame(&long_header, Bytes::from_static(b"123456789")).is_err(),
882            "PING payloads longer than 8 bytes must be rejected"
883        );
884    }
885
886    #[tokio::test]
887    async fn h2_reference_unavailable_still_runs_live_ping_assertions() {
888        let tester = PingConformanceTester::new();
889        let report = tester.run_all_tests().await;
890
891        assert_eq!(report.total_cases, 7);
892        assert_eq!(report.summary.passed, 0);
893        assert_eq!(report.summary.failed, 0);
894        assert_eq!(report.summary.expected_failures, 7);
895        assert_eq!(report.summary.skipped, 0);
896        assert_eq!(report.summary.compliance_score, 0.0);
897        assert!(
898            report
899                .results
900                .iter()
901                .all(|result| result.h2_state.is_none()),
902            "h2 reference is intentionally not wired for this harness"
903        );
904        assert!(
905            report
906                .results
907                .iter()
908                .all(|result| result.asupersync_state.is_some()),
909            "every case must exercise the live asupersync connection"
910        );
911    }
912
913    #[tokio::test]
914    async fn h2_reference_gap_is_reported_as_expected_failure_not_pass() {
915        let tester = PingConformanceTester::new();
916        let report = tester.run_all_tests().await;
917
918        assert!(
919            report
920                .results
921                .iter()
922                .all(|result| result.verdict == PingTestVerdict::ExpectedFailure),
923            "unwired h2 vendor parity must not be reported as full pass: {:?}",
924            report.results
925        );
926        assert!(
927            report.results.iter().all(|result| result
928                .error
929                .as_deref()
930                .is_some_and(|error| error.contains(H2_REFERENCE_UNAVAILABLE)
931                    && error.contains("vendor parity remains unexercised"))),
932            "each expected failure should explain the missing h2 reference parity"
933        );
934    }
935}