Add property test for AMQP serializer and parser

This commit is contained in:
David Ansari 2024-04-29 09:16:15 +02:00
parent 9f42e40346
commit 6018155e9b
10 changed files with 505 additions and 26 deletions

View File

@ -14,12 +14,12 @@
/logs/
/plugins/
/plugins.lock
/rebar.config
/rebar.lock
/sbin/
/sbin.lock
/test/ct.cover.spec
/xrefr
_build
/amqp10_common.d
/*.plt

View File

@ -110,10 +110,12 @@ dialyze(
rabbitmq_suite(
name = "binary_generator_SUITE",
size = "small",
)
rabbitmq_suite(
name = "binary_parser_SUITE",
size = "small",
)
rabbitmq_suite(
@ -121,6 +123,13 @@ rabbitmq_suite(
size = "small",
)
rabbitmq_suite(
name = "prop_SUITE",
deps = [
"//deps/rabbitmq_ct_helpers:erlang_app",
],
)
assert_suites()
alias(

View File

@ -26,6 +26,7 @@ endef
DIALYZER_OPTS += --src -r test -DTEST
BUILD_DEPS = rabbit_common
TEST_DEPS = rabbitmq_ct_helpers proper
# Variables and recipes in development.*.mk are meant to be used from
# any Git clone. They are excluded from the files published to Hex.pm.

View File

@ -110,3 +110,13 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
app_name = "amqp10_common",
erlc_opts = "//:test_erlc_opts",
)
erlang_bytecode(
name = "prop_SUITE_beam_files",
testonly = True,
srcs = ["test/prop_SUITE.erl"],
outs = ["test/prop_SUITE.beam"],
hdrs = ["include/amqp10_framing.hrl"],
app_name = "amqp10_common",
erlc_opts = "//:test_erlc_opts",
deps = ["@proper//:erlang_app"],
)

5
deps/amqp10_common/rebar.config vendored Normal file
View File

@ -0,0 +1,5 @@
{profiles,
[{test, [{deps, [proper
]}]}
]
}.

View File

@ -40,7 +40,7 @@
{symbol, binary()} |
{binary, binary()} |
{list, [amqp10_type()]} |
{map, [{amqp10_prim(), amqp10_prim()}]} | %% TODO: make map a map
{map, [{amqp10_prim(), amqp10_prim()}]} |
{array, amqp10_ctor(), [amqp10_type()]}.
-type amqp10_described() ::
@ -113,16 +113,20 @@ generate1({long, V}) when V<128 andalso V>-129 -> <<16#55,V:8/signed>>;
generate1({long, V}) -> <<16#81,V:64/signed>>;
generate1({float, V}) -> <<16#72,V:32/float>>;
generate1({double, V}) -> <<16#82,V:64/float>>;
generate1({char, V}) -> <<16#73,V:4/binary>>;
generate1({char,V}) when V>=0 andalso V=<16#10ffff -> <<16#73,V:32>>;
%% AMQP timestamp is "64-bit two's-complement integer representing milliseconds since the unix epoch".
%% For small integers (i.e. values that can be stored in a single word),
%% Erlang uses twos complement to represent the signed integers.
generate1({timestamp,V}) -> <<16#83,V:64/signed>>;
generate1({uuid, V}) -> <<16#98,V:16/binary>>;
generate1({utf8, V}) when size(V) < ?VAR_1_LIMIT -> [16#a1, size(V), V];
generate1({utf8, V}) -> [<<16#b1, (size(V)):32>>, V];
generate1({symbol, V}) -> [16#a3, size(V), V];
generate1({utf8, V}) when size(V) =< ?VAR_1_LIMIT -> [16#a1, size(V), V];
generate1({utf8, V}) -> [<<16#b1, (size(V)):32>>, V];
generate1({symbol, V}) when size(V) =< ?VAR_1_LIMIT -> [16#a3, size(V), V];
generate1({symbol, V}) -> [<<16#b3, (size(V)):32>>, V];
generate1({binary, V}) ->
Size = iolist_size(V),
case Size < ?VAR_1_LIMIT of
case Size =< ?VAR_1_LIMIT of
true ->
[16#a0, Size, V];
false ->
@ -195,6 +199,7 @@ constructor(uuid) -> 16#98;
constructor(null) -> 16#40;
constructor(boolean) -> 16#56;
constructor(array) -> 16#f0; % use large array type for all nested arrays
constructor(binary) -> 16#b0;
constructor(utf8) -> 16#b1;
constructor({described, Descriptor, Primitive}) ->
[16#00, generate1(Descriptor), constructor(Primitive)].
@ -202,10 +207,13 @@ constructor({described, Descriptor, Primitive}) ->
% returns io_list
generate2(symbol, {symbol, V}) -> [<<(size(V)):32>>, V];
generate2(utf8, {utf8, V}) -> [<<(size(V)):32>>, V];
generate2(binary, {binary, V}) -> [<<(size(V)):32>>, V];
generate2(boolean, true) -> 16#01;
generate2(boolean, false) -> 16#00;
generate2(boolean, {boolean, true}) -> 16#01;
generate2(boolean, {boolean, false}) -> 16#00;
generate2(null, null) -> 16#40;
generate2(char, {char,V}) when V>=0 andalso V=<16#10ffff -> <<V:32>>;
generate2(ubyte, {ubyte, V}) -> V;
generate2(byte, {byte, V}) -> <<V:8/signed>>;
generate2(ushort, {ushort, V}) -> <<V:16/unsigned>>;
@ -214,6 +222,10 @@ generate2(uint, {uint, V}) -> <<V:32/unsigned>>;
generate2(int, {int, V}) -> <<V:32/signed>>;
generate2(ulong, {ulong, V}) -> <<V:64/unsigned>>;
generate2(long, {long, V}) -> <<V:64/signed>>;
generate2(float, {float, V}) -> <<V:32/float>>;
generate2(double, {double, V}) -> <<V:64/float>>;
generate2(timestamp, {timestamp,V}) -> <<V:64/signed>>;
generate2(uuid, {uuid, V}) -> <<V:16/binary>>;
generate2({described, D, P}, {described, D, V}) ->
generate2(P, V);
generate2(array, {array, Type, List}) ->

View File

@ -33,7 +33,6 @@
-export_type([opts/0]).
%% Parses only the 1st AMQP type (including possible nested AMQP types).
-spec parse(binary()) ->
{amqp10_binary_generator:amqp10_type(), BytesParsed :: non_neg_integer()}.
@ -64,7 +63,7 @@ parse(<<16#61, V:16/signed, _/binary>>, B) -> {{short, V}, B+3};
parse(<<16#70, V:32/unsigned, _/binary>>, B) -> {{uint, V}, B+5};
parse(<<16#71, V:32/signed, _/binary>>, B) -> {{int, V}, B+5};
parse(<<16#72, V:32/float, _/binary>>, B) -> {{float, V}, B+5};
parse(<<16#73, Utf32:4/binary,_/binary>>, B) -> {{char, Utf32}, B+5};
parse(<<16#73, V:32, _/binary>>, B) -> {{char, V}, B+5};
parse(<<?CODE_ULONG, V:64/unsigned, _/binary>>, B) -> {{ulong, V},B+9};
parse(<<16#81, V:64/signed, _/binary>>, B) -> {{long, V}, B+9};
parse(<<16#82, V:64/float, _/binary>>, B) -> {{double, V}, B+9};
@ -110,15 +109,6 @@ parse(<<16#94, V:128, _/binary>>, B) ->
parse(<<Type, _/binary>>, B) ->
throw({primitive_type_unsupported, Type, {position, B}}).
parse_array_primitive(16#40, <<_:8/unsigned, _/binary>>) -> {null, 1};
parse_array_primitive(16#41, <<_:8/unsigned, _/binary>>) -> {true, 1};
parse_array_primitive(16#42, <<_:8/unsigned, _/binary>>) -> {false, 1};
parse_array_primitive(16#43, <<_:8/unsigned, _/binary>>) -> {{uint, 0}, 1};
parse_array_primitive(16#44, <<_:8/unsigned, _/binary>>) -> {{ulong, 0}, 1};
parse_array_primitive(ElementType, Data) ->
{Val, B} = parse(<<ElementType, Data/binary>>),
{Val, B-1}.
%% array structure is {array, Ctor, [Data]}
%% e.g. {array, symbol, [<<"amqp:accepted:list">>]}
parse_array(UnitSize, Bin) ->
@ -150,7 +140,9 @@ parse_array2(Count, Type, Bin, Acc) ->
parse_constructor(?CODE_SYM_8) -> symbol;
parse_constructor(?CODE_SYM_32) -> symbol;
parse_constructor(16#a0) -> binary;
parse_constructor(16#a1) -> utf8;
parse_constructor(16#b0) -> binary;
parse_constructor(16#b1) -> utf8;
parse_constructor(16#50) -> ubyte;
parse_constructor(16#51) -> byte;
@ -158,15 +150,29 @@ parse_constructor(16#60) -> ushort;
parse_constructor(16#61) -> short;
parse_constructor(16#70) -> uint;
parse_constructor(16#71) -> int;
parse_constructor(16#72) -> float;
parse_constructor(16#73) -> char;
parse_constructor(16#82) -> double;
parse_constructor(?CODE_ULONG) -> ulong;
parse_constructor(16#81) -> long;
parse_constructor(16#40) -> null;
parse_constructor(16#56) -> boolean;
parse_constructor(16#f0) -> array;
parse_constructor(16#83) -> timestamp;
parse_constructor(16#98) -> uuid;
parse_constructor(0) -> described; %%TODO add test with descriptor in constructor
parse_constructor(X) ->
exit({failed_to_parse_constructor, X}).
parse_array_primitive(16#40, <<_:8/unsigned, _/binary>>) -> {null, 1};
parse_array_primitive(16#41, <<_:8/unsigned, _/binary>>) -> {true, 1};
parse_array_primitive(16#42, <<_:8/unsigned, _/binary>>) -> {false, 1};
parse_array_primitive(16#43, <<_:8/unsigned, _/binary>>) -> {{uint, 0}, 1};
parse_array_primitive(16#44, <<_:8/unsigned, _/binary>>) -> {{ulong, 0}, 1};
parse_array_primitive(ElementType, Data) ->
{Val, B} = parse(<<ElementType, Data/binary>>),
{Val, B-1}.
mapify([]) ->
[];
mapify([Key, Value | Rest]) ->
@ -220,8 +226,8 @@ pm(<<16#c1, S:8,CountAndValue:S/binary,R/binary>>, O, B) ->
%% We avoid guard tests: they improve readability, but result in worse performance.
%%
%% In server mode:
%% * stop when we reach the message body (data or amqp-sequence or amqp-value section).
%% * include number of bytes left for properties and application-properties sections.
%% * Stop when we reach the message body (data or amqp-sequence or amqp-value section).
%% * Include byte positions for parsed bare message sections.
pm(<<?DESCRIBED, ?CODE_SMALL_ULONG, ?DESCRIPTOR_CODE_DATA, _Rest/binary>>, true, B) ->
reached_body(B, ?DESCRIPTOR_CODE_DATA);
pm(<<?DESCRIBED, ?CODE_SMALL_ULONG, ?DESCRIPTOR_CODE_AMQP_SEQUENCE, _Rest/binary>>, true, B) ->
@ -288,7 +294,7 @@ pm(<<16#60, V:16/unsigned, R/binary>>, O, B) -> [{ushort, V} | pm(R, O, B+3)];
pm(<<16#61, V:16/signed, R/binary>>, O, B) -> [{short, V} | pm(R, O, B+3)];
pm(<<16#71, V:32/signed, R/binary>>, O, B) -> [{int, V} | pm(R, O, B+5)];
pm(<<16#72, V:32/float, R/binary>>, O, B) -> [{float, V} | pm(R, O, B+5)];
pm(<<16#73, Utf32:4/binary,R/binary>>, O, B) -> [{char, Utf32} | pm(R, O, B+5)];
pm(<<16#73, V:32, R/binary>>, O, B) -> [{char, V} | pm(R, O, B+5)];
pm(<<16#81, V:64/signed, R/binary>>, O, B) -> [{long, V} | pm(R, O, B+9)];
pm(<<16#82, V:64/float, R/binary>>, O, B) -> [{double, V} | pm(R, O, B+9)];
pm(<<16#83, TS:64/signed, R/binary>>, O, B) -> [{timestamp, TS} | pm(R, O, B+9)];

View File

@ -113,7 +113,7 @@ utf8(_Config) ->
ok.
char(_Config) ->
roundtrip({char, <<$A/utf32>>}),
roundtrip({char, $🎉}),
ok.
list(_Config) ->

436
deps/amqp10_common/test/prop_SUITE.erl vendored Normal file
View File

@ -0,0 +1,436 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
-module(prop_SUITE).
-compile([export_all, nowarn_export_all]).
-include_lib("proper/include/proper.hrl").
-include("amqp10_framing.hrl").
-import(rabbit_ct_proper_helpers, [run_proper/3]).
all() ->
[{group, tests}].
groups() ->
[
{tests, [parallel],
[
prop_single_primitive_type_parse,
prop_single_primitive_type_parse_many,
prop_many_primitive_types_parse,
prop_many_primitive_types_parse_many,
prop_annotated_message,
prop_server_mode_body,
prop_server_mode_bare_message
]}
].
%%%%%%%%%%%%%%%%%%
%%% Properties %%%
%%%%%%%%%%%%%%%%%%
prop_single_primitive_type_parse(_Config) ->
run_proper(
fun() -> ?FORALL(Val,
oneof(primitive_types()),
begin
Bin = iolist_to_binary(amqp10_binary_generator:generate(Val)),
equals({Val, size(Bin)}, amqp10_binary_parser:parse(Bin))
end)
end, [], 10_000).
prop_single_primitive_type_parse_many(_Config) ->
run_proper(
fun() -> ?FORALL(Val,
oneof(primitive_types()),
begin
Bin = iolist_to_binary(amqp10_binary_generator:generate(Val)),
equals([Val], amqp10_binary_parser:parse_many(Bin, []))
end)
end, [], 10_000).
prop_many_primitive_types_parse(_Config) ->
run_proper(
fun() -> ?FORALL(Vals,
list(oneof(primitive_types())),
begin
Bin = iolist_to_binary([amqp10_binary_generator:generate(V) || V <- Vals]),
PosValList = parse(Bin, 0, []),
equals(Vals, [Val || {_Pos, Val} <- PosValList])
end)
end, [], 1000).
prop_many_primitive_types_parse_many(_Config) ->
run_proper(
fun() -> ?FORALL(Vals,
list(oneof(primitive_types())),
begin
Bin = iolist_to_binary([amqp10_binary_generator:generate(V) || V <- Vals]),
equals(Vals, amqp10_binary_parser:parse_many(Bin, []))
end)
end, [], 1000).
prop_annotated_message(_Config) ->
run_proper(
fun() -> ?FORALL(Sections,
annotated_message(),
begin
Bin = iolist_to_binary([amqp10_framing:encode_bin(S) || S <- Sections]),
equals(Sections, amqp10_framing:decode_bin(Bin))
end)
end, [], 1000).
prop_server_mode_body(_Config) ->
run_proper(
fun() -> ?FORALL(Sections,
annotated_message(),
begin
{value,
FirstBodySection} = lists:search(
fun(#'v1_0.data'{}) -> true;
(#'v1_0.amqp_sequence'{}) -> true;
(#'v1_0.amqp_value'{}) -> true;
(_) -> false
end, Sections),
Bin = iolist_to_binary([amqp10_framing:encode_bin(S) || S <- Sections]),
%% Invariant 1: Decoder should us return the correct
%% byte position of the first body section.
Decoded = amqp10_framing:decode_bin(Bin, [server_mode]),
{value,
{{pos, Pos},
{body, Code}}} = lists:search(fun(({{pos, _Pos}, {body, _Code}})) ->
true;
(_) ->
false
end, Decoded),
FirstBodySectionBin = binary_part(Bin, Pos, size(Bin) - Pos),
{Section, _NumBytes} = amqp10_binary_parser:parse(FirstBodySectionBin),
%% Invariant 2: Decoder should have returned the
%% correct descriptor code of the first body section.
{described, {ulong, Code}, _Val} = Section,
equals(FirstBodySection, amqp10_framing:decode(Section))
end)
end, [], 1000).
prop_server_mode_bare_message(_Config) ->
run_proper(
fun() -> ?FORALL(Sections,
annotated_message(),
begin
{value,
FirstBareMsgSection} = lists:search(
fun(#'v1_0.properties'{}) -> true;
(#'v1_0.application_properties'{}) -> true;
(#'v1_0.data'{}) -> true;
(#'v1_0.amqp_sequence'{}) -> true;
(#'v1_0.amqp_value'{}) -> true;
(_) -> false
end, Sections),
Bin = iolist_to_binary([amqp10_framing:encode_bin(S) || S <- Sections]),
%% Invariant: Decoder should us return the correct
%% byte position of the first bare message section.
Decoded = amqp10_framing:decode_bin(Bin, [server_mode]),
{value,
{{pos, Pos}, _Sect}} = lists:search(fun(({{pos, _Pos}, _Sect})) ->
true;
(_) ->
false
end, Decoded),
FirstBareMsgSectionBin = binary_part(Bin, Pos, size(Bin) - Pos),
{Section, _NumBytes} = amqp10_binary_parser:parse(FirstBareMsgSectionBin),
equals(FirstBareMsgSection, amqp10_framing:decode(Section))
end)
end, [], 1000).
%%%%%%%%%%%%%%%
%%% Helpers %%%
%%%%%%%%%%%%%%%
parse(Bin, Parsed, PosVal)
when size(Bin) =:= Parsed ->
lists:reverse(PosVal);
parse(Bin, Parsed, PosVal)
when size(Bin) > Parsed ->
BinPart = binary_part(Bin, Parsed, size(Bin) - Parsed),
{Val, NumBytes} = amqp10_binary_parser:parse(BinPart),
parse(Bin, Parsed + NumBytes, [{Parsed, Val} | PosVal]).
%%%%%%%%%%%%%%%%%%
%%% Generators %%%
%%%%%%%%%%%%%%%%%%
primitive_types() ->
fixed_and_variable_width_types() ++
compound_types() ++
[amqp_array()].
fixed_and_variable_width_types() ->
[
amqp_null(),
amqp_boolean(),
amqp_ubyte(),
amqp_ushort(),
amqp_uint(),
amqp_ulong(),
amqp_byte(),
amqp_short(),
amqp_int(),
amqp_long(),
amqp_float(),
amqp_double(),
amqp_char(),
amqp_timestamp(),
amqp_uuid(),
amqp_binary(),
amqp_string(),
amqp_symbol()
].
compound_types() ->
[
amqp_list(),
amqp_map()
].
amqp_null() ->
null.
amqp_boolean() ->
boolean().
amqp_ubyte() ->
{ubyte, integer(0, 16#ff)}.
amqp_ushort() ->
{ushort, integer(0, 16#ff_ff)}.
amqp_uint() ->
Lim = 16#ff_ff_ff_ff,
{uint, oneof([
integer(0, Lim),
?SIZED(Size, resize(Size * 100, integer(0, Lim)))
])}.
amqp_ulong() ->
Lim = 16#ff_ff_ff_ff_ff_ff_ff_ff,
{ulong, oneof([
integer(0, Lim),
?SIZED(Size, resize(Size * 100_000, integer(0, Lim)))
])}.
amqp_byte() ->
Lim = 16#ff div 2,
{byte, integer(-Lim - 1, Lim)}.
amqp_short() ->
Lim = 16#ff_ff div 2,
{short, integer(-Lim - 1, Lim)}.
amqp_int() ->
Lim = 16#ff_ff_ff_ff div 2,
{int, oneof([
integer(-Lim - 1, Lim),
?SIZED(Size, resize(Size * 100, integer(-Lim - 1, Lim)))
])}.
amqp_long() ->
Lim = 16#ff_ff_ff_ff_ff_ff_ff_ff div 2,
{long, oneof([
integer(-Lim - 1, Lim),
?SIZED(Size, resize(Size * 100, integer(-Lim - 1, Lim)))
])}.
%% AMQP float is 32-bit whereas Erlang float is 64-bit.
%% Therefore, 32-bit encoding any Erlang float will lose precision.
%% Hence, we use some static floats where we know that they can be represented precisely using 32 bits.
amqp_float() ->
{float, oneof([-1.5, -1.0, 0.0, 1.0, 1.5, 100.0])}.
%% AMQP double and Erlang float are both 64-bit.
amqp_double() ->
{double, float()}.
amqp_char() ->
{char, char()}.
amqp_timestamp() ->
Now = erlang:system_time(millisecond),
YearMillis = 1000 * 60 * 60 * 24 * 365,
TimestampMillis1950 = -631_152_000_000,
TimestampMillis2200 = 7_258_118_400_000,
{timestamp, oneof([integer(Now - YearMillis, Now + YearMillis),
integer(TimestampMillis1950, TimestampMillis2200)
])}.
amqp_uuid() ->
{uuid, binary(16)}.
amqp_binary() ->
{binary, oneof([
binary(),
?SIZED(Size, resize(Size * 10, binary()))
])}.
amqp_string() ->
{utf8, utf8()}.
amqp_symbol() ->
{symbol, ?LET(L,
?SIZED(Size, resize(Size * 10, list(ascii_char()))),
list_to_binary(L))}.
ascii_char() ->
integer(0, 127).
amqp_list() ->
{list, list(prefer_simple_type())}.
amqp_map() ->
{map, ?LET(KvList,
list({prefer_simple_type(),
prefer_simple_type()}),
lists:uniq(fun({K, _V}) -> K end, KvList)
)}.
amqp_array() ->
Gens = fixed_and_variable_width_types(),
?LET(N,
integer(1, length(Gens)),
begin
Gen = lists:nth(N, Gens),
?LET(Instance,
Gen,
begin
Constructor = case Instance of
{T, _V} -> T;
null -> null;
V when is_boolean(V) -> boolean
end,
{array, Constructor, list(Gen)}
end)
end).
prefer_simple_type() ->
frequency([{200, oneof(fixed_and_variable_width_types())},
{1, ?LAZY(oneof(compound_types()))},
{1, ?LAZY(amqp_array())}
]).
zero_or_one(Section) ->
oneof([
[],
[Section]
]).
optional(Field) ->
oneof([
undefined,
Field
]).
annotated_message() ->
?LET(H,
zero_or_one(header_section()),
?LET(DA,
zero_or_one(delivery_annotation_section()),
?LET(MA,
zero_or_one(message_annotation_section()),
?LET(P,
zero_or_one(properties_section()),
?LET(AP,
zero_or_one(application_properties_section()),
?LET(B,
body(),
?LET(F,
zero_or_one(footer_section()),
lists:append([H, DA, MA, P, AP, B, F])
))))))).
%% "The body consists of one of the following three choices: one or more data sections,
%% one or more amqp-sequence sections, or a single amqp-value section." [§3.2]
body() ->
oneof([
non_empty(list(data_section())),
non_empty(list(amqp_sequence_section())),
[amqp_value_section()]
]).
header_section() ->
#'v1_0.header'{
durable = optional(amqp_boolean()),
priority = optional(amqp_ubyte()),
ttl = optional(milliseconds()),
first_acquirer = optional(amqp_boolean()),
delivery_count = optional(amqp_uint())}.
delivery_annotation_section() ->
#'v1_0.delivery_annotations'{content = annotations()}.
message_annotation_section() ->
#'v1_0.message_annotations'{content = annotations()}.
properties_section() ->
#'v1_0.properties'{
message_id = optional(message_id()),
user_id = optional(amqp_binary()),
to = optional(address_string()),
subject = optional(amqp_string()),
reply_to = optional(address_string()),
correlation_id = optional(message_id()),
content_type = optional(amqp_symbol()),
content_encoding = optional(amqp_symbol()),
absolute_expiry_time = optional(amqp_timestamp()),
creation_time = optional(amqp_timestamp()),
group_id = optional(amqp_string()),
group_sequence = optional(sequence_no()),
reply_to_group_id = optional(amqp_string())}.
application_properties_section() ->
Gen = ?LET(KvList,
list({amqp_string(),
oneof(fixed_and_variable_width_types() -- [amqp_null()])}),
lists:uniq(fun({K, _V}) -> K end, KvList)),
#'v1_0.application_properties'{content = Gen}.
data_section() ->
#'v1_0.data'{content = binary()}.
amqp_sequence_section() ->
#'v1_0.amqp_sequence'{content = list(oneof(primitive_types() -- [amqp_null()]))}.
amqp_value_section() ->
#'v1_0.amqp_value'{content = oneof(primitive_types())}.
footer_section() ->
#'v1_0.footer'{content = annotations()}.
annotations() ->
?LET(KvList,
list({oneof([amqp_symbol(),
amqp_ulong()]),
prefer_simple_type()}),
begin
KvList1 = lists:uniq(fun({K, _V}) -> K end, KvList),
lists:filter(fun({_K, V}) -> V =/= null end, KvList1)
end).
sequence_no() ->
amqp_uint().
milliseconds() ->
amqp_uint().
message_id() ->
oneof([amqp_ulong(),
amqp_uuid(),
amqp_binary(),
amqp_string()]).
address_string() ->
amqp_string().

View File

@ -3656,8 +3656,8 @@ footer_checksum(FooterOpt, Config) ->
#'v1_0.data'{content = <<"m6 b">>},
#'v1_0.footer'{
content = [
{{symbol, <<"x-opt-rabbit">>}, {char, unicode:characters_to_binary("🐇", utf8, utf32)}},
{{symbol, <<"x-opt-carrot">>}, {char, unicode:characters_to_binary("🥕", utf8, utf32)}}
{{symbol, <<"x-opt-rabbit">>}, {char, $🐇}},
{{symbol, <<"x-opt-carrot">>}, {char, $🥕}}
]}]),
ok = amqp10_client:send_msg(Sender, M6),
ok = wait_for_settlement(<<"t6">>),
@ -3665,8 +3665,8 @@ footer_checksum(FooterOpt, Config) ->
{ok, Msg6} = amqp10_client:get_msg(Receiver),
?assertEqual([<<"m6 a">>, <<"m6 b">>], amqp10_msg:body(Msg6)),
?assertMatch(#{ExpectedKey := _,
<<"x-opt-rabbit">> := <<"🐇"/utf32>>,
<<"x-opt-carrot">> := <<"🥕"/utf32>>},
<<"x-opt-rabbit">> := $🐇,
<<"x-opt-carrot">> := $🥕},
amqp10_msg:footer(Msg6)),
%% We only sanity check here that the footer annotation we received from the server