mte/unikernel/duniverse/ocaml-h2/lib_test/test_frames.ml
2025-11-11 02:07:51 +01:00

339 lines
12 KiB
OCaml

open H2__
open Test_common
module Json = Yojson.Basic.Util
let fixtures_dir = "http2-frame-test-case"
let success_fixtures, error_fixtures =
fixtures_dir
|> Sys.readdir
|> Array.to_list
|> List.map (fun dir -> dir, Filename.concat fixtures_dir dir)
|> List.filter (fun (_, dir) -> Sys.is_directory dir)
|> List.map (fun (dir, fullpath) ->
let files_in_dir =
fullpath
|> Sys.readdir
|> Array.to_list
|> List.map (fun file -> Filename.concat fullpath file)
|> List.filter (fun file ->
(not (Sys.is_directory file)) && Filename.extension file = ".json")
in
dir, files_in_dir)
|> List.partition (fun (dir, _) -> dir <> "error")
module P = struct
open Parse
let parse_fn wire success_handler error_handler =
let handler = function
| Ok frame -> success_handler frame
| Error e -> error_handler e
in
let reader =
Reader.server_frames
~max_frame_size:Settings.default.max_frame_size
(fun _ -> ignore)
handler
in
handle_preface reader;
let wire_bs = wire |> string_of_hex |> bs_of_string in
let _read =
Reader.read_with_more
reader
wire_bs
~off:0
~len:(Bigstringaf.length wire_bs)
Incomplete
in
reader
let parse_success wire handler =
let reader =
parse_fn wire handler (fun _ ->
Alcotest.(fail "Expected to have thrown an error parsing frame."))
in
match Reader.next reader with
| `Read -> ()
| `Close | `Error _ ->
Alcotest.(fail "Expected to have sucessfully parsed frame.")
let parse_error wire handler =
let reader =
parse_fn
wire
(fun _ ->
Alcotest.(fail "Expected to have thrown an error parsing frame."))
handler
in
match Reader.next reader with
| `Read -> ()
| `Close ->
Alcotest.(fail "Expected to have thrown an error parsing frame.")
| `Error e -> handler e
let serialize ?padding frame =
let output = Test_common.serialize_frame_string ?padding frame in
hex_of_string output
end
let frame_testable =
(module struct
type t = Frame.t
let priority_to_yojson priority =
if priority != Priority.default_priority
then
let { Priority.exclusive; stream_dependency; weight } = priority in
[ "stream_dependency", `Int (Int32.to_int stream_dependency)
; "weight", `Int weight
; "exclusive", `Bool exclusive
]
else [ "stream_dependency", `Null; "weight", `Null; "exclusive", `Null ]
let bs_to_string bs =
let off = 0 in
let len = Bigstringaf.length bs in
Bigstringaf.substring ~off ~len bs
let frame_payload_to_json frame_type payload =
let others =
match payload with
| Frame.Data data -> [ "data", `String (bs_to_string data) ]
| Headers (priority, fragment) ->
("header_block_fragment", `String (bs_to_string fragment))
:: priority_to_yojson priority
| Priority priority -> priority_to_yojson priority
| RSTStream error_code ->
[ "error_code", `Int (Error_code.serialize error_code |> Int32.to_int)
]
| Settings settings_list ->
[ ( "settings"
, `List
(List.map
(fun setting ->
`List
[ `Int (Settings.serialize_key setting)
; `Int
(match setting with
| MaxConcurrentStreams value
| InitialWindowSize value ->
Int32.to_int value
| HeaderTableSize value
| EnablePush value
| MaxFrameSize value
| MaxHeaderListSize value ->
value)
])
settings_list) )
]
| PushPromise (stream_identifier, fragment) ->
[ "header_block_fragment", `String (bs_to_string fragment)
; "promised_stream_id", `Int (Int32.to_int stream_identifier)
]
| Ping data -> [ "opaque_data", `String (bs_to_string data) ]
| GoAway (stream_identifier, error_code, debug_data) ->
[ "error_code", `Int (Error_code.serialize error_code |> Int32.to_int)
; "additional_debug_data", `String (bs_to_string debug_data)
; "last_stream_id", `Int (Int32.to_int stream_identifier)
]
| WindowUpdate increment ->
[ "window_size_increment", `Int (Int32.to_int increment) ]
| Continuation fragment ->
[ "header_block_fragment", `String (bs_to_string fragment) ]
| Unknown (_frame_type, _) -> assert false
in
`Assoc (("type", `Int (Frame.FrameType.serialize frame_type)) :: others)
let frame_to_yojson { Frame.frame_header; frame_payload } =
let { Frame.payload_length; flags; stream_id; frame_type } =
frame_header
in
`Assoc
[ "length", `Int payload_length
; "flags", `Int flags
; "stream_identifier", `Int (Int32.to_int stream_id)
; "frame_payload", frame_payload_to_json frame_type frame_payload
]
let pp formatter t =
Format.pp_print_text
formatter
(t |> frame_to_yojson |> Yojson.Safe.pretty_to_string)
let equal = ( = )
end : Alcotest.TESTABLE
with type t = Frame.t)
let priority_of_json json =
let stream_dependency =
Json.(json |> member "stream_dependency" |> to_int_option)
in
let weight = Json.(json |> member "weight" |> to_int_option) in
let exclusive = Json.(json |> member "exclusive" |> to_bool_option) in
match stream_dependency, weight, exclusive with
| Some stream_dependency, Some weight, Some exclusive ->
{ Priority.exclusive
; stream_dependency = Int32.of_int stream_dependency
; weight
}
| _ -> Priority.default_priority
let frame_type_of_string = function
| "data" -> Frame.FrameType.Data
| "headers" -> Headers
| "priority" -> Priority
| "rst_stream" -> RSTStream
| "settings" -> Settings
| "push_promise" -> PushPromise
| "ping" -> Ping
| "goaway" -> GoAway
| "window_update" -> WindowUpdate
| "continuation" -> Continuation
| _ -> assert false
let frame_payload_of_json frame_type json =
match frame_type with
| Frame.FrameType.Data ->
Frame.Data Json.(json |> member "data" |> to_string |> bs_of_string)
| Headers ->
let priority = priority_of_json json in
let fragment =
Json.(json |> member "header_block_fragment" |> to_string |> bs_of_string)
in
Headers (priority, fragment)
| Priority -> Priority (priority_of_json json)
| RSTStream ->
let error_code =
Json.(json |> member "error_code" |> to_int |> Int32.of_int)
in
RSTStream (Error_code.parse error_code)
| Settings ->
let settings =
List.map
(fun setting_json ->
let setting = Json.to_list setting_json in
let key_value = List.nth setting 1 |> Json.to_int in
match Json.to_int (List.hd setting) with
| 0x1 -> Settings.HeaderTableSize key_value
| 0x2 -> EnablePush key_value
| 0x3 -> MaxConcurrentStreams (Int32.of_int key_value)
| 0x4 -> InitialWindowSize (Int32.of_int key_value)
| 0x5 -> MaxFrameSize key_value
| 0x6 -> MaxHeaderListSize key_value
| _ -> raise (Invalid_argument "settings key id"))
Json.(json |> member "settings" |> to_list)
in
Settings settings
| PushPromise ->
let fragment =
Json.(json |> member "header_block_fragment" |> to_string |> bs_of_string)
in
let promised_stream_id =
Json.(json |> member "promised_stream_id" |> to_int |> Int32.of_int)
in
PushPromise (promised_stream_id, fragment)
| Ping ->
let opaque_data =
Json.(json |> member "opaque_data" |> to_string |> bs_of_string)
in
Ping opaque_data
| GoAway ->
let error_code =
Json.(json |> member "error_code" |> to_int |> Int32.of_int)
in
let last_stream_id =
Json.(json |> member "last_stream_id" |> to_int |> Int32.of_int)
in
let debug_data =
Json.(json |> member "additional_debug_data" |> to_string |> bs_of_string)
in
GoAway (last_stream_id, Error_code.parse error_code, debug_data)
| WindowUpdate ->
let window_size_increment =
Json.(json |> member "window_size_increment" |> to_int) |> Int32.of_int
in
WindowUpdate window_size_increment
| Continuation ->
let fragment =
Json.(json |> member "header_block_fragment" |> to_string |> bs_of_string)
in
Continuation fragment
| Unknown _ -> assert false
let success_suites =
let gen_suite ~frame_type filename =
let fixture = read_all filename |> Yojson.Basic.from_string in
let test_case_name = Json.(fixture |> member "description" |> to_string) in
( Printf.sprintf "%s: %s" filename test_case_name
, `Quick
, fun () ->
let expected_frame_json = Json.(member "frame" fixture) in
let frame_payload_json =
Json.(expected_frame_json |> member "frame_payload")
in
let frame_type = frame_type_of_string frame_type in
let expected_frame =
{ Frame.frame_header =
{ payload_length =
Json.(expected_frame_json |> member "length" |> to_int)
; flags = Json.(expected_frame_json |> member "flags" |> to_int)
; stream_id =
Json.(
expected_frame_json
|> member "stream_identifier"
|> to_int
|> Int32.of_int)
; frame_type
}
; frame_payload = frame_payload_of_json frame_type frame_payload_json
}
in
let wire = Json.(fixture |> member "wire" |> to_string) in
P.parse_success wire (fun frame ->
Alcotest.check frame_testable "Frames are equal" expected_frame frame;
let padding =
match
Json.(frame_payload_json |> member "padding" |> to_string_option)
with
| Some padding ->
Some
Bigstringaf.(
of_string ~off:0 ~len:(String.length padding) padding)
| None -> None
in
let serialized_wire = P.serialize ?padding frame in
Alcotest.(
check string "Parse / Serialize roundtripping" wire serialized_wire))
)
in
List.map
(fun (name, files) ->
let suite_name = Printf.sprintf "%s frame" name in
suite_name, List.map (gen_suite ~frame_type:name) files)
success_fixtures
let error_suites =
let gen_suite filename =
let fixture = read_all filename |> Yojson.Basic.from_string in
let test_case_name = Json.(fixture |> member "description" |> to_string) in
( Printf.sprintf "%s: %s" filename test_case_name
, `Quick
, fun () ->
let possible_errors = Json.(member "error" fixture |> to_list) in
let expected_error =
possible_errors |> List.hd |> Json.to_int |> Int32.of_int
in
let wire = Json.(fixture |> member "wire" |> to_string) in
P.parse_error wire (function
| Error.ConnectionError (e, _) | Error.StreamError (_, e) ->
Alcotest.(check int32)
"Expected Error Code"
expected_error
(Error_code.serialize e)) )
in
List.map
(fun (_, files) -> "Error tests", List.map gen_suite files)
error_fixtures
let () = Alcotest.run "lambda-runtime" (success_suites @ error_suites)