340 lines
12 KiB
OCaml
340 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)
|