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)