Skip to main content

asupersync_conformance/
h2_continuation_conformance.rs

1//! HTTP/2 CONTINUATION frame conformance testing.
2//!
3//! This harness tests the `asupersync` HTTP/2 implementation's CONTINUATION
4//! frame handling against the `h2` reference implementation, specifically
5//! focusing on the requirement that CONTINUATION frames must immediately
6//! follow HEADERS/PUSH_PROMISE frames without intervening frames.
7
8use asupersync::bytes::Bytes;
9use asupersync::http::h2::frame::{
10    ContinuationFrame, DataFrame, HeadersFrame, PingFrame, SettingsFrame, WindowUpdateFrame,
11};
12use asupersync::http::h2::{Connection, ErrorCode, Frame, Settings};
13use serde::{Deserialize, Serialize};
14use std::fmt;
15
16/// Test verdict for CONTINUATION conformance cases.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum ContinuationTestVerdict {
19    Pass,
20    Fail,
21    ExpectedFailure, // Known divergence
22    Skipped,
23}
24
25impl fmt::Display for ContinuationTestVerdict {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::Pass => write!(f, "PASS"),
29            Self::Fail => write!(f, "FAIL"),
30            Self::ExpectedFailure => write!(f, "XFAIL"),
31            Self::Skipped => write!(f, "SKIP"),
32        }
33    }
34}
35
36/// Requirement level for CONTINUATION conformance.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum RequirementLevel {
39    Must,   // RFC MUST
40    Should, // RFC SHOULD
41    May,    // RFC MAY
42}
43
44/// Test frame sequence for CONTINUATION testing.
45#[derive(Debug, Clone)]
46pub struct FrameSequence {
47    pub name: String,
48    pub description: String,
49    pub frames: Vec<TestFrame>,
50    pub expected_error: Option<ErrorCode>,
51}
52
53/// Individual test frame.
54#[derive(Debug, Clone)]
55pub struct TestFrame {
56    pub frame_type: String,
57    pub stream_id: u32,
58    pub payload: Vec<u8>,
59    pub flags: u8,
60    pub description: String,
61}
62
63/// Single CONTINUATION conformance test case.
64#[derive(Debug, Clone)]
65pub struct ContinuationConformanceCase {
66    pub id: String,
67    pub description: String,
68    pub requirement_level: RequirementLevel,
69    pub frame_sequence: FrameSequence,
70    pub expected_outcome: ExpectedOutcome,
71}
72
73/// Expected outcome for a CONTINUATION test.
74#[derive(Debug, Clone, PartialEq)]
75pub enum ExpectedOutcome {
76    /// Connection should accept the frame sequence.
77    Accept,
78    /// Connection should reject with PROTOCOL_ERROR.
79    ProtocolError,
80    /// Connection should reject with specific error code.
81    ErrorCode(ErrorCode),
82}
83
84/// Result of running a single CONTINUATION test case.
85#[derive(Debug, Clone, Serialize)]
86pub struct ContinuationTestResult {
87    pub case_id: String,
88    pub verdict: ContinuationTestVerdict,
89    pub error: Option<String>,
90    pub asupersync_result: TestFrameResult,
91    pub expected_result: TestFrameResult,
92    pub error_codes_match: bool,
93}
94
95/// Result of processing a frame sequence on a connection.
96#[derive(Debug, Clone, Serialize)]
97pub struct TestFrameResult {
98    pub accepted: bool,
99    pub error_code: Option<String>,
100    pub error_message: Option<String>,
101    pub frames_processed: usize,
102}
103
104/// Summary statistics for a CONTINUATION conformance run.
105#[derive(Debug, Clone, Serialize)]
106pub struct ContinuationComplianceSummary {
107    pub passed: usize,
108    pub failed: usize,
109    pub expected_failures: usize,
110    pub skipped: usize,
111    pub total: usize,
112    pub compliance_score: f64,
113}
114
115/// Complete report for CONTINUATION conformance testing.
116#[derive(Debug, Clone, Serialize)]
117pub struct ContinuationComplianceReport {
118    pub test_run_id: String,
119    pub timestamp: String,
120    pub total_cases: usize,
121    pub results: Vec<ContinuationTestResult>,
122    pub summary: ContinuationComplianceSummary,
123}
124
125/// CONTINUATION conformance tester.
126pub struct ContinuationConformanceTester {
127    pub test_cases: Vec<ContinuationConformanceCase>,
128}
129
130impl ContinuationConformanceTester {
131    /// Create a new CONTINUATION conformance tester with standard test cases.
132    pub fn new() -> Self {
133        Self {
134            test_cases: Self::create_test_cases(),
135        }
136    }
137
138    /// Create the standard set of CONTINUATION conformance test cases.
139    fn create_test_cases() -> Vec<ContinuationConformanceCase> {
140        vec![
141            ContinuationConformanceCase {
142                id: "CONT-001".to_string(),
143                description: "HEADERS+CONTINUATION with intervening PING should cause PROTOCOL_ERROR".to_string(),
144                requirement_level: RequirementLevel::Must,
145                frame_sequence: FrameSequence {
146                    name: "headers-ping-continuation".to_string(),
147                    description: "HEADERS without END_HEADERS, then PING, then CONTINUATION".to_string(),
148                    frames: vec![
149                        TestFrame {
150                            frame_type: "HEADERS".to_string(),
151                            stream_id: 1,
152                            payload: create_partial_header_block(),
153                            flags: 0x00, // No END_HEADERS (0x04)
154                            description: "HEADERS frame without END_HEADERS flag".to_string(),
155                        },
156                        TestFrame {
157                            frame_type: "PING".to_string(),
158                            stream_id: 0,
159                            payload: vec![0, 1, 2, 3, 4, 5, 6, 7],
160                            flags: 0x00,
161                            description: "PING frame (invalid - should be CONTINUATION)".to_string(),
162                        },
163                        TestFrame {
164                            frame_type: "CONTINUATION".to_string(),
165                            stream_id: 1,
166                            payload: vec![0x00], // Empty continuation
167                            flags: 0x04, // END_HEADERS
168                            description: "CONTINUATION frame with END_HEADERS".to_string(),
169                        },
170                    ],
171                    expected_error: Some(ErrorCode::ProtocolError),
172                },
173                expected_outcome: ExpectedOutcome::ProtocolError,
174            },
175            ContinuationConformanceCase {
176                id: "CONT-002".to_string(),
177                description: "HEADERS+CONTINUATION with intervening SETTINGS should cause PROTOCOL_ERROR".to_string(),
178                requirement_level: RequirementLevel::Must,
179                frame_sequence: FrameSequence {
180                    name: "headers-settings-continuation".to_string(),
181                    description: "HEADERS without END_HEADERS, then SETTINGS, then CONTINUATION".to_string(),
182                    frames: vec![
183                        TestFrame {
184                            frame_type: "HEADERS".to_string(),
185                            stream_id: 1,
186                            payload: create_partial_header_block(),
187                            flags: 0x00, // No END_HEADERS
188                            description: "HEADERS frame without END_HEADERS flag".to_string(),
189                        },
190                        TestFrame {
191                            frame_type: "SETTINGS".to_string(),
192                            stream_id: 0,
193                            payload: vec![], // Empty settings
194                            flags: 0x00,
195                            description: "SETTINGS frame (invalid - should be CONTINUATION)".to_string(),
196                        },
197                        TestFrame {
198                            frame_type: "CONTINUATION".to_string(),
199                            stream_id: 1,
200                            payload: vec![0x00],
201                            flags: 0x04, // END_HEADERS
202                            description: "CONTINUATION frame with END_HEADERS".to_string(),
203                        },
204                    ],
205                    expected_error: Some(ErrorCode::ProtocolError),
206                },
207                expected_outcome: ExpectedOutcome::ProtocolError,
208            },
209            ContinuationConformanceCase {
210                id: "CONT-003".to_string(),
211                description: "HEADERS+CONTINUATION with intervening DATA should cause PROTOCOL_ERROR".to_string(),
212                requirement_level: RequirementLevel::Must,
213                frame_sequence: FrameSequence {
214                    name: "headers-data-continuation".to_string(),
215                    description: "HEADERS without END_HEADERS, then DATA, then CONTINUATION".to_string(),
216                    frames: vec![
217                        TestFrame {
218                            frame_type: "HEADERS".to_string(),
219                            stream_id: 1,
220                            payload: create_partial_header_block(),
221                            flags: 0x00, // No END_HEADERS
222                            description: "HEADERS frame without END_HEADERS flag".to_string(),
223                        },
224                        TestFrame {
225                            frame_type: "DATA".to_string(),
226                            stream_id: 1,
227                            payload: b"hello".to_vec(),
228                            flags: 0x00,
229                            description: "DATA frame (invalid - should be CONTINUATION)".to_string(),
230                        },
231                        TestFrame {
232                            frame_type: "CONTINUATION".to_string(),
233                            stream_id: 1,
234                            payload: vec![0x00],
235                            flags: 0x04, // END_HEADERS
236                            description: "CONTINUATION frame with END_HEADERS".to_string(),
237                        },
238                    ],
239                    expected_error: Some(ErrorCode::ProtocolError),
240                },
241                expected_outcome: ExpectedOutcome::ProtocolError,
242            },
243            ContinuationConformanceCase {
244                id: "CONT-004".to_string(),
245                description: "HEADERS+CONTINUATION with intervening WINDOW_UPDATE should cause PROTOCOL_ERROR".to_string(),
246                requirement_level: RequirementLevel::Must,
247                frame_sequence: FrameSequence {
248                    name: "headers-window-update-continuation".to_string(),
249                    description: "HEADERS without END_HEADERS, then WINDOW_UPDATE, then CONTINUATION".to_string(),
250                    frames: vec![
251                        TestFrame {
252                            frame_type: "HEADERS".to_string(),
253                            stream_id: 1,
254                            payload: create_partial_header_block(),
255                            flags: 0x00, // No END_HEADERS
256                            description: "HEADERS frame without END_HEADERS flag".to_string(),
257                        },
258                        TestFrame {
259                            frame_type: "WINDOW_UPDATE".to_string(),
260                            stream_id: 1,
261                            payload: vec![0x00, 0x00, 0x04, 0x00], // Increment by 1024
262                            flags: 0x00,
263                            description: "WINDOW_UPDATE frame (invalid - should be CONTINUATION)".to_string(),
264                        },
265                        TestFrame {
266                            frame_type: "CONTINUATION".to_string(),
267                            stream_id: 1,
268                            payload: vec![0x00],
269                            flags: 0x04, // END_HEADERS
270                            description: "CONTINUATION frame with END_HEADERS".to_string(),
271                        },
272                    ],
273                    expected_error: Some(ErrorCode::ProtocolError),
274                },
275                expected_outcome: ExpectedOutcome::ProtocolError,
276            },
277            ContinuationConformanceCase {
278                id: "CONT-005".to_string(),
279                description: "Valid HEADERS+CONTINUATION sequence should be accepted".to_string(),
280                requirement_level: RequirementLevel::Must,
281                frame_sequence: FrameSequence {
282                    name: "headers-continuation-valid".to_string(),
283                    description: "HEADERS without END_HEADERS, immediately followed by CONTINUATION".to_string(),
284                    frames: vec![
285                        TestFrame {
286                            frame_type: "HEADERS".to_string(),
287                            stream_id: 1,
288                            payload: create_partial_header_block(),
289                            flags: 0x00, // No END_HEADERS
290                            description: "HEADERS frame without END_HEADERS flag".to_string(),
291                        },
292                        TestFrame {
293                            frame_type: "CONTINUATION".to_string(),
294                            stream_id: 1,
295                            payload: vec![0x00],
296                            flags: 0x04, // END_HEADERS
297                            description: "CONTINUATION frame with END_HEADERS".to_string(),
298                        },
299                    ],
300                    expected_error: None,
301                },
302                expected_outcome: ExpectedOutcome::Accept,
303            },
304            ContinuationConformanceCase {
305                id: "CONT-006".to_string(),
306                description: "CONTINUATION for wrong stream ID should cause PROTOCOL_ERROR".to_string(),
307                requirement_level: RequirementLevel::Must,
308                frame_sequence: FrameSequence {
309                    name: "headers-continuation-wrong-stream".to_string(),
310                    description: "HEADERS on stream 1, CONTINUATION on stream 3".to_string(),
311                    frames: vec![
312                        TestFrame {
313                            frame_type: "HEADERS".to_string(),
314                            stream_id: 1,
315                            payload: create_partial_header_block(),
316                            flags: 0x00, // No END_HEADERS
317                            description: "HEADERS frame without END_HEADERS flag".to_string(),
318                        },
319                        TestFrame {
320                            frame_type: "CONTINUATION".to_string(),
321                            stream_id: 3, // Wrong stream ID
322                            payload: vec![0x00],
323                            flags: 0x04, // END_HEADERS
324                            description: "CONTINUATION frame with wrong stream ID".to_string(),
325                        },
326                    ],
327                    expected_error: Some(ErrorCode::ProtocolError),
328                },
329                expected_outcome: ExpectedOutcome::ProtocolError,
330            },
331        ]
332    }
333
334    /// Run all conformance test cases.
335    pub async fn run_all_tests(&mut self) -> ContinuationComplianceReport {
336        let test_run_id = uuid::Uuid::new_v4().to_string();
337        let timestamp = chrono::Utc::now().to_rfc3339();
338        let total_cases = self.test_cases.len();
339        let mut results = Vec::new();
340
341        for test_case in &self.test_cases {
342            let result = self.run_single_test(test_case).await;
343            results.push(result);
344        }
345
346        let summary = self.compute_summary(&results);
347
348        ContinuationComplianceReport {
349            test_run_id,
350            timestamp,
351            total_cases,
352            results,
353            summary,
354        }
355    }
356
357    /// Run a single CONTINUATION conformance test case.
358    async fn run_single_test(&self, case: &ContinuationConformanceCase) -> ContinuationTestResult {
359        // Test our implementation
360        let asupersync_result = self
361            .test_asupersync_implementation(&case.frame_sequence)
362            .await;
363
364        // Determine expected result
365        let expected_result = match &case.expected_outcome {
366            ExpectedOutcome::Accept => TestFrameResult {
367                accepted: true,
368                error_code: None,
369                error_message: None,
370                frames_processed: case.frame_sequence.frames.len(),
371            },
372            ExpectedOutcome::ProtocolError => TestFrameResult {
373                accepted: false,
374                error_code: Some("PROTOCOL_ERROR".to_string()),
375                error_message: Some("expected CONTINUATION frame".to_string()),
376                frames_processed: 1, // Should fail after first non-CONTINUATION frame
377            },
378            ExpectedOutcome::ErrorCode(code) => TestFrameResult {
379                accepted: false,
380                error_code: Some(format!("{:?}", code)),
381                error_message: None,
382                frames_processed: 1,
383            },
384        };
385
386        let error_codes_match = match (&asupersync_result.error_code, &expected_result.error_code) {
387            (Some(actual), Some(expected)) => actual == expected,
388            (None, None) => true,
389            _ => false,
390        };
391
392        let verdict = if asupersync_result.accepted == expected_result.accepted && error_codes_match
393        {
394            ContinuationTestVerdict::Pass
395        } else {
396            ContinuationTestVerdict::Fail
397        };
398
399        let error = if verdict == ContinuationTestVerdict::Fail {
400            Some(format!(
401                "Expected {}, got {}. Error codes: expected {:?}, actual {:?}",
402                if expected_result.accepted {
403                    "ACCEPT"
404                } else {
405                    "REJECT"
406                },
407                if asupersync_result.accepted {
408                    "ACCEPT"
409                } else {
410                    "REJECT"
411                },
412                expected_result.error_code,
413                asupersync_result.error_code
414            ))
415        } else {
416            None
417        };
418
419        ContinuationTestResult {
420            case_id: case.id.clone(),
421            verdict,
422            error,
423            asupersync_result,
424            expected_result,
425            error_codes_match,
426        }
427    }
428
429    /// Test frame sequence against asupersync implementation.
430    async fn test_asupersync_implementation(&self, sequence: &FrameSequence) -> TestFrameResult {
431        // Create a server connection for testing
432        let mut connection = Connection::server(Settings::default());
433        if let Err(error) = connection.process_frame(Frame::Settings(SettingsFrame::new(vec![]))) {
434            return TestFrameResult {
435                accepted: false,
436                error_code: Some(format!("{:?}", error.code)),
437                error_message: Some(error.message),
438                frames_processed: 0,
439            };
440        }
441
442        let mut frames_processed = 0;
443        let mut last_error: Option<String> = None;
444        let mut last_error_code: Option<String> = None;
445
446        for test_frame in &sequence.frames {
447            frames_processed += 1;
448
449            // Convert test frame to actual HTTP/2 frame
450            let frame = match self.create_h2_frame(test_frame) {
451                Ok(f) => f,
452                Err(e) => {
453                    last_error = Some(format!("Frame creation failed: {}", e));
454                    break;
455                }
456            };
457
458            // Process the frame
459            match connection.process_frame(frame) {
460                Ok(_) => {
461                    // Frame was accepted, continue
462                }
463                Err(error) => {
464                    // Frame was rejected
465                    last_error_code = Some(format!("{:?}", error.code));
466                    last_error = Some(error.message);
467                    break;
468                }
469            }
470        }
471
472        let accepted = last_error.is_none();
473
474        TestFrameResult {
475            accepted,
476            error_code: last_error_code,
477            error_message: last_error,
478            frames_processed: if accepted {
479                frames_processed
480            } else {
481                frames_processed.saturating_sub(1)
482            },
483        }
484    }
485
486    /// Create an actual HTTP/2 frame from test frame description.
487    fn create_h2_frame(&self, test_frame: &TestFrame) -> Result<Frame, String> {
488        match test_frame.frame_type.as_str() {
489            "HEADERS" => {
490                Ok(Frame::Headers(HeadersFrame::new(
491                    test_frame.stream_id,
492                    Bytes::copy_from_slice(&test_frame.payload),
493                    false,                        // end_stream
494                    test_frame.flags & 0x04 != 0, // end_headers
495                )))
496            }
497            "CONTINUATION" => Ok(Frame::Continuation(ContinuationFrame {
498                stream_id: test_frame.stream_id,
499                header_block: Bytes::copy_from_slice(&test_frame.payload),
500                end_headers: test_frame.flags & 0x04 != 0,
501            })),
502            "PING" => {
503                if test_frame.payload.len() >= 8 {
504                    let mut data = [0u8; 8];
505                    data.copy_from_slice(&test_frame.payload[..8]);
506                    Ok(Frame::Ping(PingFrame::new(data)))
507                } else {
508                    Err("PING frame needs 8 bytes of payload".to_string())
509                }
510            }
511            "SETTINGS" => Ok(Frame::Settings(SettingsFrame::new(vec![]))),
512            "DATA" => {
513                Ok(Frame::Data(DataFrame::new(
514                    test_frame.stream_id,
515                    Bytes::copy_from_slice(&test_frame.payload),
516                    test_frame.flags & 0x01 != 0, // end_stream
517                )))
518            }
519            "WINDOW_UPDATE" => {
520                if test_frame.payload.len() >= 4 {
521                    let increment = u32::from_be_bytes([
522                        test_frame.payload[0],
523                        test_frame.payload[1],
524                        test_frame.payload[2],
525                        test_frame.payload[3],
526                    ]);
527                    Ok(Frame::WindowUpdate(WindowUpdateFrame::new(
528                        test_frame.stream_id,
529                        increment,
530                    )))
531                } else {
532                    Err("WINDOW_UPDATE frame needs 4 bytes of payload".to_string())
533                }
534            }
535            _ => Err(format!("Unknown frame type: {}", test_frame.frame_type)),
536        }
537    }
538
539    /// Compute summary statistics from test results.
540    fn compute_summary(&self, results: &[ContinuationTestResult]) -> ContinuationComplianceSummary {
541        let total = results.len();
542        let passed = results
543            .iter()
544            .filter(|r| r.verdict == ContinuationTestVerdict::Pass)
545            .count();
546        let failed = results
547            .iter()
548            .filter(|r| r.verdict == ContinuationTestVerdict::Fail)
549            .count();
550        let expected_failures = results
551            .iter()
552            .filter(|r| r.verdict == ContinuationTestVerdict::ExpectedFailure)
553            .count();
554        let skipped = results
555            .iter()
556            .filter(|r| r.verdict == ContinuationTestVerdict::Skipped)
557            .count();
558
559        let compliance_score = if passed + failed > 0 {
560            passed as f64 / (passed + failed) as f64
561        } else {
562            1.0
563        };
564
565        ContinuationComplianceSummary {
566            passed,
567            failed,
568            expected_failures,
569            skipped,
570            total,
571            compliance_score,
572        }
573    }
574
575    /// Generate a markdown report from the compliance results.
576    pub fn generate_markdown_report(&self, report: &ContinuationComplianceReport) -> String {
577        let mut output = String::new();
578
579        output.push_str("# HTTP/2 CONTINUATION Frame Conformance Report\n\n");
580        output.push_str(&format!("**Test Run ID:** {}\n", report.test_run_id));
581        output.push_str(&format!("**Timestamp:** {}\n", report.timestamp));
582        output.push_str(&format!("**Total Test Cases:** {}\n\n", report.total_cases));
583
584        output.push_str("## Summary\n\n");
585        output.push_str(&format!(
586            "- ✅ **Passed:** {} tests\n",
587            report.summary.passed
588        ));
589        output.push_str(&format!(
590            "- ❌ **Failed:** {} tests\n",
591            report.summary.failed
592        ));
593        output.push_str(&format!(
594            "- ⚠️  **Expected Failures:** {} tests\n",
595            report.summary.expected_failures
596        ));
597        output.push_str(&format!(
598            "- ⏭️  **Skipped:** {} tests\n",
599            report.summary.skipped
600        ));
601        output.push_str(&format!(
602            "- 🎯 **Compliance Score:** {:.1}%\n\n",
603            report.summary.compliance_score * 100.0
604        ));
605
606        if report.summary.failed > 0 {
607            output.push_str("## Failed Test Cases\n\n");
608            for result in &report.results {
609                if result.verdict == ContinuationTestVerdict::Fail {
610                    output.push_str(&format!("### {}\n", result.case_id));
611                    if let Some(error) = &result.error {
612                        output.push_str(&format!("**Error:** {}\n", error));
613                    }
614                    output.push_str(&format!(
615                        "**Error codes match:** {}\n\n",
616                        result.error_codes_match
617                    ));
618                }
619            }
620        }
621
622        output.push_str("## All Test Results\n\n");
623        output.push_str("| Case ID | Verdict | Error Codes Match | Error |\n");
624        output.push_str("|---------|---------|-------------------|-------|\n");
625
626        for result in &report.results {
627            let error_str = result.error.as_deref().unwrap_or("-");
628            output.push_str(&format!(
629                "| {} | {} | {} | {} |\n",
630                result.case_id, result.verdict, result.error_codes_match, error_str
631            ));
632        }
633
634        output
635    }
636}
637
638impl Default for ContinuationConformanceTester {
639    fn default() -> Self {
640        Self::new()
641    }
642}
643
644/// Create a minimal header block for testing (just enough to be valid HPACK).
645fn create_partial_header_block() -> Vec<u8> {
646    // Simple HPACK-encoded header block for ":method: GET"
647    // Using indexed header field representation (RFC 7541 Section 6.1)
648    // Index 2 in static table is ":method: GET"
649    vec![0x82] // 10000010 = indexed header field with index 2
650}