1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum PingTestVerdict {
26 Pass,
27 Fail,
28 ExpectedFailure, 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub enum RequirementLevel {
46 Must, Should, May, }
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct PingTiming {
54 pub sent_at_ms: u64,
56 pub ack_received_at_ms: Option<u64>,
58 pub rtt_ms: Option<u64>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct PingConnectionState {
65 pub connection_state: String,
67 pub pending_ping_acks: usize,
69 pub ping_timings: Vec<PingTiming>,
71 pub has_errors: bool,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SerializablePingFrame {
78 pub opaque_data: [u8; 8],
79 pub ack: bool,
80 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, }
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#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PingConformanceCase {
106 pub id: String,
107 pub description: String,
108 pub requirement_level: RequirementLevel,
109 pub ping_sequence: Vec<SerializablePingFrame>,
111 pub expected_connection_state: PingConnectionState,
113 pub expected_rtt_behavior: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct PingConformanceResult {
120 pub case_id: String,
121 pub verdict: PingTestVerdict,
122 pub error: Option<String>,
123 pub asupersync_state: Option<PingConnectionState>,
125 pub h2_state: Option<PingConnectionState>,
127 pub differences: Vec<String>,
129}
130
131#[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, }
141
142#[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 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#[derive(Debug)]
200pub struct PingConformanceTester {
201 pub test_cases: Vec<PingConformanceCase>,
202}
203
204impl PingConformanceTester {
205 pub fn new() -> Self {
207 Self {
208 test_cases: create_ping_test_cases(),
209 }
210 }
211
212 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 async fn run_single_test(&self, case: &PingConformanceCase) -> PingConformanceResult {
226 let asupersync_result = self.test_asupersync_ping(case).await;
228
229 let h2_result = self.test_h2_ping(case).await;
233
234 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 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 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 for serializable_frame in &case.ping_sequence {
340 let ping_frame: PingFrame = serializable_frame.clone().into();
341
342 if !ping_frame.ack {
343 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 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 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 let connection_state = extract_asupersync_ping_state(&mut connection, ping_timings)?;
380 Ok(connection_state)
381 }
382
383 async fn test_h2_ping(
385 &self,
386 _case: &PingConformanceCase,
387 ) -> Result<PingConnectionState, String> {
388 Err(H2_REFERENCE_UNAVAILABLE.to_string())
389 }
390
391 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 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 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 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 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
535fn 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
551fn 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
577fn create_ping_test_cases() -> Vec<PingConformanceCase> {
579 vec![
580 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, },
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 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 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 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 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 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 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 ],
728 expected_connection_state: PingConnectionState {
729 connection_state: "Open".to_string(),
730 pending_ping_acks: 1, 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 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, 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, },
756 expected_rtt_behavior: "Orphan PING_ACK ignored gracefully".to_string(),
757 },
758 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 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 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(), 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}