1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum ContinuationTestVerdict {
19 Pass,
20 Fail,
21 ExpectedFailure, 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum RequirementLevel {
39 Must, Should, May, }
43
44#[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#[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#[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#[derive(Debug, Clone, PartialEq)]
75pub enum ExpectedOutcome {
76 Accept,
78 ProtocolError,
80 ErrorCode(ErrorCode),
82}
83
84#[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#[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#[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#[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
125pub struct ContinuationConformanceTester {
127 pub test_cases: Vec<ContinuationConformanceCase>,
128}
129
130impl ContinuationConformanceTester {
131 pub fn new() -> Self {
133 Self {
134 test_cases: Self::create_test_cases(),
135 }
136 }
137
138 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, 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], flags: 0x04, 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, 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![], 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, 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, 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, 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, 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], 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, 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, 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, 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, description: "HEADERS frame without END_HEADERS flag".to_string(),
318 },
319 TestFrame {
320 frame_type: "CONTINUATION".to_string(),
321 stream_id: 3, payload: vec![0x00],
323 flags: 0x04, 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 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 async fn run_single_test(&self, case: &ContinuationConformanceCase) -> ContinuationTestResult {
359 let asupersync_result = self
361 .test_asupersync_implementation(&case.frame_sequence)
362 .await;
363
364 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, },
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 async fn test_asupersync_implementation(&self, sequence: &FrameSequence) -> TestFrameResult {
431 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 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 match connection.process_frame(frame) {
460 Ok(_) => {
461 }
463 Err(error) => {
464 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 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, test_frame.flags & 0x04 != 0, )))
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, )))
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 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 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
644fn create_partial_header_block() -> Vec<u8> {
646 vec![0x82] }