Created
June 16, 2017 07:36
-
-
Save benjamintanweihao/b487b8ae522e5898f8536e05e45f052f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%%% File : gpb_eqc.erl | |
%%% Author : Thomas Arts <thomas.arts@quviq.com> | |
%%% Further developed by: Tomas Abrahamsson <tab@lysator.liu.se> | |
%%% Description : Testing protocol buffer implemented by Tomas Abrahamsson | |
%%% Created : 12 May 2010 by Thomas Arts | |
-module(gpb_eqc). | |
-include_lib("eqc/include/eqc.hrl"). | |
-include("gpb.hrl"). | |
-compile(export_all). | |
%% Eunit integration | |
qc_prop_test_() -> | |
AllProps = [Fn || {Fn,Arity} <- ?MODULE:module_info(exports), | |
is_property(atom_to_list(Fn), Arity)], | |
{Descr, PropsToTest} = | |
case find_protoc() of | |
false -> | |
{"QuickCheck tests" | |
" (note: 'protoc' not in $PATH and no PROTOC env var defined," | |
" so excluding some properties)", | |
AllProps -- [prop_encode_decode_via_protoc]}; | |
P when is_list(P) -> | |
{"QuickCheck tests", AllProps} | |
end, | |
{Descr, | |
{timeout, 600, %% timeout for all tests | |
[{timeout, 300, %% timeout for each test | |
[{atom_to_list(Prop), | |
fun() -> true = eqc:quickcheck(?MODULE:Prop()) end}]} | |
|| Prop <- PropsToTest]}}. | |
is_property("prop_"++_, 0) -> true; | |
is_property(_, _) -> false. | |
message_defs() -> | |
message_defs([]). | |
message_defs(Opts) -> | |
%% Can we have messages that refer to themselves? | |
%% Actually not if field is required, since then we cannot generate | |
%% a message of that kind. | |
%% left_of/1 guarantees that the messages only refer to earlier definitions | |
%% Enums are globally unique. Hence, we generate them globally | |
DoAny = not lists:member(no_any, Opts), | |
?LET({MsgNames,Any}, {eqc_gen:non_empty(ulist("m")), | |
elements([[], ['google.protobuf.Any' || DoAny]])}, | |
?LET(EnumDefs,enums(), | |
begin | |
AnyDef = [mk_anymsg_def() || Any /= []], | |
EnumNames = [EName || {{enum,EName},_}<-EnumDefs], | |
Enum0Names = [EName || {{enum,EName},[{_,0}|_]}<-EnumDefs], | |
shuffle(EnumDefs | |
++ AnyDef | |
++ [{{msg,Msg},message_fields( | |
left_of(Msg,Any++MsgNames), | |
EnumNames, Enum0Names, | |
Opts)} | |
|| Msg<-MsgNames]) | |
end)). | |
mk_anymsg_def() -> | |
{{msg,'google.protobuf.Any'}, | |
[#?gpb_field{name=type_url, type=string, | |
fnum=1, rnum=2, occurrence=required, opts=[]}, | |
#?gpb_field{name=value, type=bytes, | |
fnum=2, rnum=3, occurrence=required, opts=[]}]}. | |
%% Take all values left of a certain value | |
left_of(X,Xs) -> | |
lists:takewhile(fun(Y) -> | |
Y/=X | |
end,Xs). | |
message_fields(MsgNames, EnumNames, Enum0Names, Opts) -> | |
%% can we have definitions without any field? | |
DoMapFields = not lists:member(no_maps, Opts), | |
?LET({FieldDefs, FNumBase0}, | |
{eqc_gen:non_empty( | |
list({elements([{required, msg_field_type(MsgNames, EnumNames)}, | |
{optional, msg_field_type(MsgNames, EnumNames)}, | |
{repeated, msg_field_type(MsgNames, EnumNames)}, | |
{oneof, oneof_fields(MsgNames, EnumNames)}] | |
++ [{repeated, map_field(MsgNames, Enum0Names)} | |
|| DoMapFields]), | |
field_name()})), | |
uint(10)}, | |
mk_fields(FieldDefs, FNumBase0+1)). | |
message_name() -> | |
elements([m1,m2,m3,m4,m5,m6]). | |
field_name() -> | |
elements([a,b,c,field1,f]). | |
oneof_fields(MsgNames, EnumNames) -> | |
?LET({FieldDefs, FNumBase0}, | |
{eqc_gen:non_empty( | |
list({elements([{optional, msg_field_type(MsgNames, EnumNames)}]), | |
field_name()})), | |
uint(10)}, | |
mk_fields(FieldDefs, FNumBase0+1)). | |
map_field(MsgNames, EnumNames) -> | |
?LET({KeyType,ValueType}, {elements(map_key_types()), | |
msg_field_type(MsgNames, EnumNames)}, | |
{map,KeyType,ValueType}). | |
msg_field_type([], []) -> | |
elements(basic_msg_field_types()); | |
msg_field_type([], EnumNames) -> | |
?LET(EnumName,elements(EnumNames), | |
elements(basic_msg_field_types() ++ [{enum, EnumName}])); | |
msg_field_type(MsgNames, []) -> | |
?LET(MsgName,elements(MsgNames), | |
elements(basic_msg_field_types() ++ [{'msg',MsgName}])); | |
msg_field_type(MsgNames, EnumNames) -> | |
?LET({MsgName, EnumName}, {elements(MsgNames), elements(EnumNames)}, | |
elements(basic_msg_field_types() ++ | |
[{enum, EnumName}, {'msg',MsgName}])). | |
basic_msg_field_types() -> | |
[bool,sint32,sint64,int32,int64,uint32, | |
uint64, | |
fixed64,sfixed64,double, | |
fixed32, | |
sfixed32, | |
float, | |
bytes, | |
string | |
]. | |
map_key_types() -> | |
[bool,sint32,sint64,int32,int64,uint32, | |
uint64, | |
fixed64,sfixed64, | |
fixed32, | |
sfixed32, | |
string | |
]. | |
mk_fields(FieldDefs, FNumBase) -> | |
UFieldDefs = keyunique(2, FieldDefs), | |
{Fields, _NextFNum} = | |
lists:mapfoldl( | |
fun({{{required, Type}, FieldName}, RNum}, FNum) -> | |
{#?gpb_field{name=FieldName, fnum=FNum, rnum=RNum, | |
type=Type, occurrence=required, opts=[]}, | |
FNum+1}; | |
({{{optional, Type}, FieldName}, RNum}, FNum) -> | |
{#?gpb_field{name=FieldName, fnum=FNum, rnum=RNum, | |
type=Type, occurrence=optional, opts=[]}, | |
FNum+1}; | |
({{{repeated, Type}, FieldName}, RNum}, FNum) -> | |
Opts = case Type of | |
{map,_,_} -> []; | |
{msg,_} -> []; %% FIXME: why not packed? | |
string -> []; %% FIXME: why not packed? | |
bytes -> []; %% FIXME: why not packed? | |
_ -> elements([[], [packed]]) | |
end, | |
{#?gpb_field{name=FieldName, fnum=FNum, rnum=RNum, | |
type=Type, occurrence=repeated, opts=Opts}, | |
FNum+1}; | |
({{{oneof, OFields}, FieldName}, RNum}, FNum) -> | |
%% Oneof fields, must have unique names and field numbers | |
%% (within the message) | |
{OFields2, NewFNum} = | |
lists:mapfoldl( | |
fun(#?gpb_field{name=ONm}=F, OFNum) -> | |
OFieldName = combine_name(FieldName, ONm), | |
{F#?gpb_field{name=OFieldName, | |
rnum=RNum, fnum=OFNum}, | |
OFNum+1} | |
end, | |
FNum, | |
OFields), | |
{#gpb_oneof{name=FieldName, rnum=RNum, fields=OFields2}, | |
NewFNum} | |
end, | |
FNumBase, | |
seq_index(UFieldDefs, 2)), | |
Fields. | |
keyunique(_N, []) -> | |
[]; | |
keyunique(N, [Tuple|Rest]) -> | |
[Tuple| keyunique(N, [ T2 || T2<-Rest, element(N,T2)/=element(N,Tuple)])]. | |
seq_index(L, Start) -> | |
lists:zip(L, lists:seq(1+(Start-1),length(L)+(Start-1))). | |
combine_name(NameA, NameB) -> | |
list_to_atom(lists:concat([NameA, "_", NameB])). | |
%% In fact, we should improve this to have different enums containing same value | |
%% e.g. [ {{enum,e1},[{x1,10}]}, {{enum,x2},[{x2,10}]} ] | |
enums() -> | |
?LET({N,Values,Names},{int(),ulist("x"),ulist("e")}, | |
?LET(Constants,unique_values(Values,N), | |
enums(Names,Constants))). | |
ulist(String) -> | |
?LET(N,nat(), | |
[ list_to_atom(String++integer_to_list(K)) || K<-lists:seq(1,N) ]). | |
%% Unique names and unqiue values | |
%% Example | |
%% enum file_open_return_values { enoent=1, eacess=2 } | |
unique_values([],_N) -> | |
[]; | |
unique_values([Cons|Conss],N) -> | |
?LET(Next,nat(), | |
[{Cons,N} | unique_values(Conss,Next+N+1)]). | |
enums([],_Conss) -> | |
[]; | |
enums(_Enames,[]) -> | |
[]; | |
enums([Ename|Enames],Conss) -> | |
?LET(Element,elements(Conss), | |
begin | |
Prefix = left_of(Element,Conss)++[Element], | |
[{{enum,Ename},Prefix}|enums(Enames,Conss--Prefix)] | |
end). | |
%% generator for messages that respect message definitions | |
message(MessageDefs,Opts) -> | |
MsgDefs = [MD || {{msg,_MsgName},_}=MD <- MessageDefs], % filter out enums | |
CandidateMsgDefs = case proplists:get_value(any_translate, Opts) of | |
undefined -> | |
MsgDefs; | |
_ -> | |
[MD || {{msg,Name},_}=MD <- MsgDefs, | |
Name /= 'google.protobuf.Any'] | |
end, | |
?LET({{msg,Msg},_Fields},oneof(CandidateMsgDefs), | |
message(Msg,MessageDefs,Opts)). | |
message(Msg,MessageDefs,Opts) -> | |
Fields = proplists:get_value({msg,Msg},MessageDefs), | |
FieldValues = [case Field of | |
#?gpb_field{} -> field_value(Field, MessageDefs, Opts); | |
#gpb_oneof{} -> oneof_value(Field, MessageDefs, Opts) | |
end | |
|| Field <- Fields], | |
list_to_tuple([Msg|FieldValues]). | |
oneof_value(#gpb_oneof{fields=OFields}, MessageDefs, Opts) -> | |
?LET(OField, oneof(OFields), | |
begin | |
#?gpb_field{name=Name} = OField, | |
oneof([undefined, | |
{Name, field_value(OField#?gpb_field{occurrence=required}, | |
MessageDefs, | |
Opts)}]) | |
end). | |
field_value(#?gpb_field{type=Type, occurrence=Occurrence}, MsgDefs, Opts) -> | |
field_val2(Type, Occurrence, MsgDefs, Opts). | |
field_val2(Type, optional, MsgDefs, Opts) -> | |
default(undefined, value(Type,MsgDefs,Opts)); | |
field_val2({map,KeyType,ValueType}, repeated, MsgDefs, Opts) -> | |
?LET(Keys, list(value(KeyType, MsgDefs, Opts)), | |
[{Key, value(ValueType, MsgDefs, Opts)} || Key <- lists:usort(Keys)]); | |
field_val2(Type, repeated, MsgDefs, Opts) -> | |
list(value(Type, MsgDefs, Opts)); | |
field_val2(Type, required, MsgDefs, Opts) -> | |
value(Type,MsgDefs, Opts). | |
value({msg,M},MessageDefs,Opts) -> | |
if M == 'google.protobuf.Any' -> | |
case proplists:get_value(any_translate,Opts) of | |
undefined -> | |
message(M,MessageDefs,Opts); | |
_ -> | |
uint(32) | |
end; | |
M /= 'google.protobuf.Any' -> | |
message(M,MessageDefs,Opts) | |
end; | |
value({enum,E},MessageDefs,_Opts) -> | |
{value, {{enum,E},EnumValues}} = lists:keysearch({enum,E}, 1, MessageDefs), | |
?LET({Symbolic, _ActualValue}, elements(EnumValues), | |
Symbolic); | |
value({map,KeyType,ValueType}, MessageDefs, Opts) -> | |
{value(KeyType, MessageDefs, Opts), value(ValueType, MessageDefs, Opts)}; | |
value(bool,_,_) -> | |
bool(); | |
value(sint32,_,_) -> | |
sint(32); | |
value(sint64,_,_) -> | |
sint(64); | |
value(int32,_,_) -> | |
int(32); | |
value(int64,_,_) -> | |
int(64); | |
value(uint32,_,_) -> | |
uint(32); | |
value(uint64,_,_) -> | |
uint(64); | |
value(fixed64,_,_) -> | |
uint(64); | |
value(sfixed64,_,_) -> | |
sint(64); | |
value(fixed32,_,_) -> | |
uint(32); | |
value(sfixed32,_,_) -> | |
sint(32); | |
value(double, _,_) -> | |
frequency([{70,real()}, {10,'infinity'}, {10,'-infinity'}, {10,nan}]); | |
value(float, _,_) -> | |
frequency([{70,real()}, {10,'infinity'}, {10,'-infinity'}, {10,nan}]); | |
value(bytes, _,_) -> | |
binary(); | |
value(string, _,_) -> | |
list(encodable_unicode_code_point()). | |
encodable_unicode_code_point() -> | |
%% http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf | |
%% * Section 3.3, D9: Code points: integers in the range 0..10ffff | |
%% * Section 3.9: The Unicode Standard supports three character | |
%% encoding forms: UTF-32, UTF-16, and UTF-8. Each encoding form | |
%% maps the Unicode code points U+0000..U+D7FF and | |
%% U+E000..U+10FFFF to unique code unit sequences | |
?SUCHTHAT(CP, oneof([uint(16), choose(16#10000, 16#10FFFF)]), | |
(0 =< CP andalso CP =< 16#d7ff) | |
orelse | |
(16#e000 =< CP andalso CP =< 16#10ffff)). | |
sint(Base) -> | |
int(Base). | |
int(Base) -> | |
?LET(I,uint(Base), | |
begin | |
<< N:Base/signed >> = <<I:Base>>, N | |
end). | |
uint(Base) -> | |
oneof([ choose(0,pow2(B)-1) || B<-lists:seq(1,Base)]). | |
pow2(0) -> 1; | |
pow2(N) when N > 0 -> 2*pow2(N-1); | |
pow2(N) when N < 0 -> 1/pow2(-N). | |
%%% properties | |
prop_encode_decode() -> | |
Mod = gpb_eqc_m, | |
?FORALL( | |
MsgDefs,message_defs(), | |
?FORALL( | |
{Encoder, Decoder, COpts}, encoder_decoder(Mod), | |
?FORALL( | |
Msg, message(MsgDefs, COpts), | |
begin | |
MsgName = element(1, Msg), | |
install_msg_defs(Mod, MsgDefs, Encoder, Decoder, COpts), | |
Bin = encode_msg(Msg, MsgDefs, Encoder, COpts), | |
DecodedMsg = decode_msg(Bin, MsgName, MsgDefs, Decoder, COpts), | |
?WHENFAIL(io:format("~p /= ~p\n",[Msg, DecodedMsg]), | |
msg_approximately_equals(Msg, DecodedMsg, | |
MsgDefs, COpts)) | |
end))). | |
%% add a round-trip via the `protoc' program in the protobuf package. | |
%% The `protoc' is the compiler generates code from a .proto file, but | |
%% it can also decode and encode messages on the fly, given a .proto | |
%% file, so we can use it as an sort of interop test. | |
prop_encode_decode_via_protoc() -> | |
MDOpts1 = case check_protoc_can_do_map_fields() of | |
true -> []; | |
false -> [no_maps] | |
end, | |
MDOpts = MDOpts1 ++ [no_any], | |
Mod = gpb_eqc_m, | |
?FORALL( | |
MsgDefs,message_defs(MDOpts), | |
?FORALL( | |
{Encoder, Decoder, COpts}, encoder_decoder(Mod, [no_any]), | |
?FORALL( | |
Msg, message(MsgDefs, COpts), | |
begin | |
MsgName = element(1, Msg), | |
install_msg_defs(Mod, MsgDefs, Encoder, Decoder, COpts), | |
TmpDir = get_create_tmpdir(), | |
install_msg_defs_as_proto(MsgDefs, TmpDir), | |
GpbBin = encode_msg(Msg, MsgDefs, Encoder, COpts), | |
ProtoBin = decode_then_reencode_via_protoc( | |
GpbBin, Msg, TmpDir), | |
DecodedMsg = decode_msg(ProtoBin, MsgName, MsgDefs, | |
Decoder, COpts), | |
?WHENFAIL(io:format("~p /= ~p\n",[Msg,DecodedMsg]), | |
msg_approximately_equals(Msg, DecodedMsg, | |
MsgDefs, COpts)) | |
end))). | |
%% test that we can ignore unknown fields | |
prop_encode_decode_with_skip() -> | |
Mod1 = gpb_eqc_m1, | |
Mod2 = gpb_eqc_m2, | |
?FORALL( | |
MsgDefs, message_defs([no_any]), | |
?FORALL( | |
InitialMsg, message(MsgDefs, []), | |
?FORALL( | |
{{SubMsg, SubDefs}, | |
{Encoder1, Decoder1, COpts1}, | |
{Encoder2, Decoder2, COpts2}}, | |
{message_subset_defs(InitialMsg, MsgDefs), | |
encoder_decoder(Mod1, [no_any]), | |
encoder_decoder(Mod2, [no_any])}, | |
begin | |
MsgName = element(1, InitialMsg), | |
install_msg_defs(Mod1, MsgDefs, Encoder1, Decoder1, COpts1), | |
install_msg_defs(Mod2, SubDefs, Encoder2, Decoder2, COpts2), | |
Encoded = encode_msg(InitialMsg, MsgDefs, Encoder1, COpts1), | |
%% now decode the byte sequence with a decoder that knows | |
%% only a subset of the fields for each message. | |
Decoded = decode_msg(Encoded, MsgName, SubDefs, Decoder2, | |
COpts2), | |
?WHENFAIL(io:format("~p /= ~p\n",[SubMsg, Decoded]), | |
msg_approximately_equals(SubMsg, Decoded, | |
SubDefs, [])) | |
end))). | |
%% test merging of messages | |
prop_merge() -> | |
Mod = gpb_eqc_m, | |
?FORALL( | |
MsgDefs, message_defs(), | |
?FORALL( | |
MsgName, oneof([M || {{msg,M},_} <- MsgDefs]), | |
?FORALL( | |
{Encoder, Decoder, COpts}, encoder_decoder(Mod), | |
?FORALL( | |
{Msg1, Msg2}, {message(MsgName, MsgDefs, COpts), | |
message(MsgName, MsgDefs, COpts)}, | |
begin | |
install_msg_defs(Mod, MsgDefs, Encoder, Decoder, COpts), | |
MergedMsg = merge_msgs(Msg1, Msg2, MsgDefs, | |
Encoder, Decoder, COpts), | |
Bin1 = encode_msg(Msg1, MsgDefs, Encoder, COpts), | |
Bin2 = encode_msg(Msg2, MsgDefs, Encoder, COpts), | |
MergedBin = <<Bin1/binary,Bin2/binary>>, | |
DecodedMerge = decode_msg(MergedBin, MsgName, MsgDefs, | |
Decoder, COpts), | |
msg_equals(MergedMsg, DecodedMerge, MsgDefs, COpts) | |
end)))). | |
%% compute a subset of the fields, and also a subset of the msg, | |
%% corresponding to the subset of the fields. | |
%% Return {SubsetMsg, SubsetDefs} | |
message_subset_defs(Msg, MsgDefs) -> | |
?LET(DefsWithSkips, | |
[case Elem of | |
{{enum,_}, _}=Enum -> | |
Enum; | |
{{msg,'google.protobuf.Any'}, _Fields}=MsgDef -> % keep intact | |
MsgDef; | |
{{msg,MsgName}, MsgFields} -> | |
{{msg, MsgName}, msg_fields_subset_skips(MsgFields)} | |
end | |
|| Elem <- MsgDefs], | |
begin | |
SubsetMsg = remove_fields_by_skips(Msg, DefsWithSkips), | |
SubsetDefs = remove_skips_from_defs(DefsWithSkips), | |
{SubsetMsg, SubsetDefs} | |
end). | |
msg_fields_subset_skips(Fields) when length(Fields) == 1 -> | |
%% can't remove anything if there's only one field | |
?LET(_, int(), | |
Fields); | |
msg_fields_subset_skips(Fields) -> | |
?LET(Fields2, [elements([Field, skip]) || Field <- Fields], | |
%% compensate for removed fields | |
Fields2). | |
remove_fields_by_skips(Msg, DefsWithSkips) -> | |
MsgName = element(1, Msg), | |
{{msg,MsgName}, MsgDef} = lists:keyfind({msg,MsgName}, 1, DefsWithSkips), | |
Fields = [case Field of | |
#?gpb_field{type={msg, _SubMsgName}, occurrence=repeated} -> | |
[remove_fields_by_skips(Elem, DefsWithSkips) | |
|| Elem <- Value]; | |
#?gpb_field{type={map,_,{msg, _SubMsgName}}} -> | |
[{K,remove_fields_by_skips(V, DefsWithSkips)} | |
|| {K, V} <- Value]; | |
#?gpb_field{type={msg, _SubMsgName}, occurrence=Occurrence} -> | |
if Occurrence == optional, Value == undefined -> | |
Value; | |
true -> | |
remove_fields_by_skips(Value, DefsWithSkips) | |
end; | |
#gpb_oneof{fields=OFields} -> | |
case Value of | |
undefined -> | |
Value; | |
{OFName, Value2} -> | |
Pos = #?gpb_field.name, | |
case lists:keyfind(OFName, Pos, OFields) of | |
#?gpb_field{type={msg, _SubMsgName}} -> | |
{OFName, remove_fields_by_skips( | |
Value2, DefsWithSkips)}; | |
_ -> | |
Value | |
end | |
end; | |
_ -> | |
Value | |
end | |
|| {Value, Field} <- lists:zip(tl(tuple_to_list(Msg)), MsgDef), | |
Field /= skip], | |
list_to_tuple([MsgName | Fields]). | |
remove_skips_from_defs(DefsWithSkips) -> | |
[case Elem of | |
{{enum,_}, _}=Enum -> | |
Enum; | |
{{msg,MsgName}, FieldsAndSkips} -> | |
{{msg, MsgName}, remove_skips_recalculate_rnums(FieldsAndSkips)} | |
end | |
|| Elem <- DefsWithSkips]. | |
remove_skips_recalculate_rnums(FieldsAndSkips) -> | |
{RecalculatedFieldsReversed, _TotalNumSkipped} = | |
lists:foldl( | |
fun(skip, {Fs, NumSkipped}) -> | |
{Fs, NumSkipped+1}; | |
(#?gpb_field{rnum=RNum}=F, {Fs, NumSkipped}) -> | |
{[F#?gpb_field{rnum=RNum-NumSkipped} | Fs], NumSkipped}; | |
(#gpb_oneof{rnum=RNum, fields=OFs}=F, {Fs, NumSkipped}) -> | |
OFs2 = [O#?gpb_field{rnum=RNum-NumSkipped} || O <- OFs], | |
F2 = F#gpb_oneof{rnum=RNum-NumSkipped, | |
fields=OFs2}, | |
{[F2 | Fs], NumSkipped} | |
end, | |
{[], 0}, | |
FieldsAndSkips), | |
lists:reverse(RecalculatedFieldsReversed). | |
encoder_decoder(Mod) -> encoder_decoder(Mod, []). | |
encoder_decoder(Mod, Opts) -> | |
DoAny = not lists:member(no_any, Opts), | |
?LET( | |
{Encoder, Decoder}, {oneof([gpb, Mod]), oneof([gpb, Mod])}, | |
?LET( | |
COpts1, | |
[{copy_bytes, oneof([false, true, auto, choose(2,4)])}, | |
{field_pass_method, oneof([pass_as_record, pass_as_params])} | |
| map_opts()], | |
?LET( | |
COpts2, any_translation_opts(COpts1), | |
if DoAny, Encoder /= gpb, Decoder /= gpb -> | |
{Encoder, Decoder, COpts1 ++ COpts2}; | |
true -> | |
{Encoder, Decoder, COpts1} | |
end))). | |
map_opts() -> | |
HaveMaps = case get(cache_have_maps) of | |
undefined -> | |
V = have_maps(), | |
put(cache_have_maps, V), | |
V; | |
V -> | |
V | |
end, | |
if HaveMaps -> | |
[{maps, oneof([false, true])}, | |
{maps_unset_optional, oneof([present_undefined, omitted])}]; | |
not HaveMaps -> | |
[] | |
end. | |
have_maps() -> | |
try maps:size(maps:new()) of | |
0 -> | |
true | |
catch error:undef -> | |
false | |
end. | |
any_translation_opts(Opts0) -> | |
DoMaps = proplists:get_bool(maps, Opts0), | |
?LET(DoTranslate, oneof([false, {true, bool(), bool()}]), | |
case DoTranslate of | |
{true, TranslateMerge, TranslateVerify} -> | |
Encode = if DoMaps -> | |
[{encode,{?MODULE,any_tr_pack_m,['$1']}}]; | |
not DoMaps -> | |
[{encode,{?MODULE,any_tr_pack_r,['$1']}}] | |
end, | |
Decode = if DoMaps -> | |
[{decode,{?MODULE,any_tr_unpack_m,['$1']}}]; | |
not DoMaps -> | |
[{decode,{?MODULE,any_tr_unpack_r,['$1']}}] | |
end, | |
Merge = [{merge,{?MODULE,any_tr_merge,['$1','$2']}} | |
|| TranslateMerge], | |
Verify = [{verify,{?MODULE,any_tr_verify,['$1','$errorf']}} | |
|| TranslateVerify], | |
[{any_translate, Encode ++ Decode ++ Merge ++ Verify}]; | |
false -> | |
[] | |
end). | |
any_tr_pack_m(N) -> | |
NStr = integer_to_list(N), | |
maps:from_list([{type_url,NStr}, | |
{value,list_to_binary(NStr)}]). | |
any_tr_unpack_m(M) -> | |
ML = maps:to_list(M), | |
NStr = proplists:get_value(type_url,ML), | |
NBin = proplists:get_value(value,ML), | |
N = list_to_integer(NStr), | |
N = list_to_integer(binary_to_list(NBin)), | |
N. | |
any_tr_pack_r(N) -> | |
NStr = integer_to_list(N), | |
{'google.protobuf.Any',NStr,list_to_binary(NStr)}. | |
any_tr_unpack_r({'google.protobuf.Any',NStr,NBin}) -> | |
N = list_to_integer(NStr), | |
N = list_to_integer(binary_to_list(NBin)), | |
N. | |
any_tr_merge(_,N2) -> | |
N2. | |
any_tr_verify(V, _ErrorF) when is_integer(V) -> ok; | |
any_tr_verify(_, ErrorF) -> ErrorF(not_an_integer). | |
encode_msg(Msg, MsgDefs, Encoder, COpts) -> | |
case Encoder of | |
gpb -> | |
gpb:encode_msg(Msg, MsgDefs); | |
_ -> | |
case proplists:get_value(maps, COpts) of | |
false -> | |
Encoder:encode_msg(Msg); | |
true -> | |
map_encode_msg(Msg, MsgDefs, Encoder, COpts) | |
end | |
end. | |
map_encode_msg(Msg, MsgDefs, Encoder, COpts) -> | |
MsgAsMap = msg_to_map(Msg, MsgDefs, COpts), | |
MsgName = element(1, Msg), | |
Encoder:encode_msg(MsgAsMap, MsgName). | |
decode_msg(Bin, MsgName, MsgDefs, Decoder, COpts) -> | |
case Decoder of | |
gpb -> | |
gpb:decode_msg(Bin, MsgName, MsgDefs); | |
_ -> | |
case proplists:get_value(maps, COpts) of | |
false -> | |
Decoder:decode_msg(Bin, MsgName); | |
true -> | |
map_decode_msg(Bin, MsgName, MsgDefs, Decoder, COpts) | |
end | |
end. | |
map_decode_msg(Bin, MsgName, MsgDefs, Decoder, COpts) -> | |
MsgAsMap = Decoder:decode_msg(Bin, MsgName), | |
map_to_msg(MsgAsMap, MsgName, MsgDefs, COpts). | |
merge_msgs(Msg1, Msg2, MsgDefs, Encoder, Decoder, COpts) -> | |
if Encoder == gpb, Decoder == gpb -> | |
gpb:merge_msgs(Msg1, Msg2, MsgDefs); | |
Encoder /= gpb -> | |
case proplists:get_value(maps, COpts) of | |
false -> | |
Encoder:merge_msgs(Msg1, Msg2); | |
true -> | |
map_merge_msgs(Msg1, Msg2, MsgDefs, Encoder, COpts) | |
end; | |
Decoder /= gpb -> | |
case proplists:get_value(maps, COpts) of | |
false -> | |
Decoder:merge_msgs(Msg1, Msg2); | |
true -> | |
map_merge_msgs(Msg1, Msg2, MsgDefs, Decoder, COpts) | |
end | |
end. | |
map_merge_msgs(Msg1, Msg2, MsgDefs, Mod, COpts) -> | |
Msg1AsMap = msg_to_map(Msg1, MsgDefs, COpts), | |
Msg2AsMap = msg_to_map(Msg2, MsgDefs, COpts), | |
MsgName = element(1, Msg1), | |
ResultAsMap = Mod:merge_msgs(Msg1AsMap, Msg2AsMap, MsgName), | |
map_to_msg(ResultAsMap, MsgName, MsgDefs, COpts). | |
install_msg_defs(Mod, MsgDefs, Encoder, Decoder, COpts) -> | |
if Encoder == gpb, Decoder == gpb -> | |
ok; %% nothing needs to be done | |
true -> | |
install_msg_defs_aux(Mod, MsgDefs, COpts) | |
end. | |
install_msg_defs(Mod, MsgDefs) -> | |
install_msg_defs_aux(Mod, MsgDefs, [{copy_bytes, auto}]). | |
install_msg_defs_aux(Mod, MsgDefs, Opts) when is_list(Opts) -> | |
Opts2 = [binary, {verify, always}, return_warnings | Opts], | |
{{ok, Mod, Code, _},_} = {gpb_compile:msg_defs(Mod, MsgDefs, Opts2), | |
compile}, | |
ok = delete_old_versions_of_code(Mod), | |
{{module, Mod},_} = {code:load_binary(Mod, "<nofile>", Code), load_code}, | |
ok. | |
delete_old_versions_of_code(Mod) -> | |
code:purge(Mod), | |
code:delete(Mod), | |
code:purge(Mod), | |
code:delete(Mod), | |
ok. | |
msg_equals(Msg1, Msg2, MsgDefs, Opts) -> | |
case msg_approximately_equals(Msg1, Msg2, MsgDefs, Opts) of | |
true -> | |
true; | |
false -> | |
%% Run equals, even though we know it'll return | |
%% false, because it'll show the messages | |
%% appropritately -- e.g. not when shrinking. | |
equals(Msg1,Msg2) | |
end. | |
msg_approximately_equals(M1, M2, MsgDefs, Opts) | |
when is_tuple(M1), is_tuple(M2), | |
element(1,M1) == element(1,M2), | |
tuple_size(M1) == tuple_size(M2) -> | |
MsgName = element(1,M1), | |
{{msg,MsgName},Fields} = lists:keyfind({msg,MsgName},1,MsgDefs), | |
lists:all(fun({F1, F2, Field}) -> | |
field_approximately_equals(F1, F2, Field, MsgDefs, Opts) | |
end, | |
lists:zip3(tl(tuple_to_list(M1)), | |
tl(tuple_to_list(M2)), | |
Fields)); | |
msg_approximately_equals(_X, _Y, _MsgDefs, _Opts) -> | |
false. | |
field_approximately_equals(F1, F2, #?gpb_field{type={map,_,VT}}, | |
MsgDefs, Opts) -> | |
DoTranslateAny = proplists:get_value(any_translate,Opts) /= undefined, | |
lists:all(fun({{K1,V1}, {K2,V2}}) -> | |
case VT of | |
{msg,'google.protobuf.Any'} when DoTranslateAny -> | |
is_value_approx_eq(K1,K2) | |
andalso | |
is_value_approx_eq(V1,V2); | |
{msg,_} -> | |
is_value_approx_eq(K1,K2) | |
andalso | |
msg_approximately_equals(V1,V2,MsgDefs,Opts); | |
_ -> | |
is_value_approx_eq(K1,K2) | |
andalso | |
is_value_approx_eq(V1,V2) | |
end | |
end, | |
lists:zip(lists:sort(F1),lists:sort(F2))); | |
field_approximately_equals(F1, F2, #?gpb_field{type={msg,MsgName}, | |
occurrence=Occ}, | |
MsgDefs, Opts) -> | |
DoTranslateAny = proplists:get_value(any_translate,Opts) /= undefined, | |
case {Occ, MsgName} of | |
{_, 'google.protobuf.Any'} when DoTranslateAny -> | |
is_value_approx_eq(F1, F2); | |
{repeated,_} -> | |
lists:all(fun({E1,E2}) -> | |
msg_approximately_equals(E1, E2, MsgDefs, Opts) | |
end, | |
lists:zip(F1,F2)); | |
{optional,_} -> | |
if F1 == undefined, F2 == undefined -> | |
true; | |
true -> | |
msg_approximately_equals(F1, F2, MsgDefs, Opts) | |
end; | |
{required,_} -> | |
msg_approximately_equals(F1, F2, MsgDefs, Opts) | |
end; | |
field_approximately_equals({T,F1}, {T,F2}, #gpb_oneof{fields=OFs}, | |
MsgDefs, Opts) -> | |
DoTranslateAny = proplists:get_value(any_translate,Opts) /= undefined, | |
case lists:keyfind(T,#?gpb_field.name,OFs) of | |
#?gpb_field{type={msg,'google.protobuf.Any'}} when DoTranslateAny -> | |
is_value_approx_eq(F1, F2); | |
#?gpb_field{type={msg,_}} -> | |
msg_approximately_equals(F1, F2, MsgDefs, Opts); | |
_ -> | |
is_value_approx_eq(F1, F2) | |
end; | |
field_approximately_equals(F1, F2, _Field, _MsgDefs, _Opts) -> | |
is_value_approx_eq(F1, F2). | |
is_value_approx_eq(F1, F2) when is_float(F1), is_float(F2) -> | |
is_float_equivalent(F1, F2); | |
is_value_approx_eq(L1, L2) when is_list(L1), is_list(L2) -> | |
lists:all(fun({E1,E2}) -> is_value_approx_eq(E1,E2) end, lists:zip(L1,L2)); | |
is_value_approx_eq(X, X) -> | |
true; | |
is_value_approx_eq(_X, _Y) -> | |
io:format("Not equal: ~p <--> ~p~n", [_X, _Y]), | |
false. | |
-define(ABS_ERROR, 1.0e-10). %% was: 1.0e-16 | |
-define(REL_ERROR, 1.0e-6). %% was: 1.0e-10 | |
is_float_equivalent(F, F) -> true; | |
is_float_equivalent(F1,F2) -> | |
if (abs(F1-F2) < ?ABS_ERROR) -> true; | |
(abs(F1) > abs(F2)) -> abs( (F1-F2)/F1 ) < ?REL_ERROR; | |
(abs(F1) < abs(F2)) -> abs( (F1-F2)/F2 ) < ?REL_ERROR | |
end. | |
is_within_percent(F1, F2, PercentsAllowedDeviation) -> | |
AllowedDeviation = PercentsAllowedDeviation / 100, | |
abs(F1 - F2) < (AllowedDeviation * F1). | |
% Recursively translate a record to a map | |
msg_to_map(Msg, MsgDefs, COpts) -> | |
MsgName = element(1, Msg), | |
{{msg,MsgName},Fields} = lists:keyfind({msg,MsgName}, 1, MsgDefs), | |
FVs = [case F of | |
#?gpb_field{name=FName}=Field -> | |
V2 = field_to_map(V, Field, MsgDefs, COpts), | |
{FName, V2}; | |
#gpb_oneof{name=FName, fields=OFields} -> | |
V2 = case V of | |
undefined -> | |
undefined; | |
{Tag, TV} -> | |
Field = lists:keyfind(Tag, #?gpb_field.name, | |
OFields), | |
{Tag, field_to_map(TV, Field, MsgDefs, COpts)} | |
end, | |
{FName, V2} | |
end | |
|| {F,V} <- lists:zip(Fields, tl(tuple_to_list(Msg)))], | |
case proplists:get_value(maps_unset_optional, COpts) of | |
present_undefined -> | |
maps:from_list(FVs); | |
omitted -> | |
maps:from_list([FV || {_,Value}=FV <- FVs, | |
Value /= undefined]) | |
end. | |
field_to_map(V, #?gpb_field{type={msg,MsgName},occurrence=Occurrence}, | |
MsgDefs,COpts) -> | |
submsg_to_map1(Occurrence, MsgName, V, MsgDefs, COpts); | |
field_to_map(V, #?gpb_field{type={map,_,ValueType}}, MsgDefs, COpts) -> | |
maps:from_list( | |
[case ValueType of | |
{msg, MsgName} -> | |
{K, submsg_to_map1(required, MsgName, V2, MsgDefs, COpts)}; | |
_ -> {K, V2} | |
end | |
|| {K,V2} <- V]); | |
field_to_map(V, _FieldDef, _MsgDefs, _COpts) -> | |
V. | |
submsg_to_map1(Occurrence, MsgName, V, MsgDefs, COpts) -> | |
if MsgName == 'google.protobuf.Any' -> | |
case proplists:get_value(any_translate, COpts) of | |
undefined -> | |
submsg_to_map2(Occurrence, V, MsgDefs, COpts); | |
_ -> | |
%% it is really an integer since we've added translations | |
%% for it | |
V | |
end; | |
true -> | |
submsg_to_map2(Occurrence, V, MsgDefs, COpts) | |
end. | |
submsg_to_map2(required, V, MsgDefs, COpts) -> | |
msg_to_map(V, MsgDefs, COpts); | |
submsg_to_map2(repeated, Seq, MsgDefs, COpts) -> | |
[msg_to_map(Elem, MsgDefs, COpts) || Elem <- Seq]; | |
submsg_to_map2(optional, V, MsgDefs, COpts) -> | |
if V == undefined -> undefined; | |
V /= undefined -> msg_to_map(V, MsgDefs, COpts) | |
end. | |
map_to_msg(Map, MsgName, MsgDefs, COpts) -> | |
{{msg,MsgName},Fields} = lists:keyfind({msg,MsgName}, 1, MsgDefs), | |
list_to_tuple( | |
[MsgName | | |
[case F of | |
#?gpb_field{name=FName}=Field -> | |
case maps:find(FName, Map) of | |
error -> | |
undefined; | |
{ok, V} -> | |
field_from_map(V, Field, MsgDefs, COpts) | |
end; | |
#gpb_oneof{name=FName, fields=OFields} -> | |
case maps:find(FName, Map) of | |
error -> | |
undefined; | |
{ok, undefined} -> | |
undefined; | |
{ok, {Tag, TV}} -> | |
Field = lists:keyfind(Tag, #?gpb_field.name, OFields), | |
{Tag, field_from_map(TV, Field, MsgDefs, COpts)} | |
end | |
end | |
|| F <- Fields]]). | |
field_from_map(V, #?gpb_field{type={msg,SubMsgName}, occurrence=Occurrence}, | |
MsgDefs, COpts) -> | |
submsg_from_map1(Occurrence, V, SubMsgName, MsgDefs, COpts); | |
field_from_map(V, #?gpb_field{type={map,_,ValueType}}, MsgDefs, COpts) -> | |
[case ValueType of | |
{msg, Name2} -> {K, submsg_from_map1(required, V2, Name2, MsgDefs, | |
COpts)}; | |
_ -> {K,V2} | |
end | |
|| {K,V2} <- maps:to_list(V)]; | |
field_from_map(V, _FieldDef, _MsgDefs, _COpts) -> | |
V. | |
submsg_from_map1(Occurrence, Map, MsgName, MsgDefs, COpts) -> | |
if MsgName == 'google.protobuf.Any' -> | |
case proplists:get_value(any_translate, COpts) of | |
undefined -> | |
submsg_from_map2(Occurrence, Map, MsgName, MsgDefs, COpts); | |
_ -> | |
%% it is really an integer since we've added translations | |
%% for it | |
Map | |
end; | |
true -> | |
submsg_from_map2(Occurrence, Map, MsgName, MsgDefs, COpts) | |
end. | |
submsg_from_map2(required, Map, MsgName, MsgDefs, COpts) -> | |
map_to_msg(Map, MsgName, MsgDefs, COpts); | |
submsg_from_map2(repeated, Seq, MsgName, MsgDefs, COpts) -> | |
[map_to_msg(Elem, MsgName, MsgDefs, COpts) || Elem <- Seq]; | |
submsg_from_map2(optional, V, MsgName, MsgDefs, COpts) -> | |
if V == undefined -> undefined; | |
V /= undefined -> map_to_msg(V, MsgName, MsgDefs, COpts) | |
end. | |
get_create_tmpdir() -> | |
D = filename:join("/tmp", f("~s-~s", [?MODULE, os:getpid()])), | |
filelib:ensure_dir(filename:join(D, "dummy-file-name")), | |
[file:delete(X) || X <- filelib:wildcard(filename:join(D,"*"))], | |
D. | |
delete_tmpdir(TmpDir) -> | |
[file:delete(X) || X <- filelib:wildcard(filename:join(TmpDir,"*"))], | |
file:del_dir(TmpDir). | |
install_msg_defs_as_proto(MsgDefs, TmpDir) -> | |
ProtoFile = filename:join(TmpDir, "x.proto"), | |
ok = file:write_file(ProtoFile, msg_defs_to_proto(MsgDefs)). | |
msg_defs_to_proto(MsgDefs) -> | |
iolist_to_binary( | |
["syntax=\"proto2\";\n", | |
lists:map(fun msg_def_to_proto/1, MsgDefs)]). | |
contains_mapfield([{{msg,_},Fields} | Rest]) -> | |
HasMapField = lists:any(fun(#?gpb_field{type={map,_,_}}) -> true; | |
(_) -> false | |
end, | |
Fields), | |
if HasMapField -> true; | |
not HasMapField -> contains_mapfield(Rest) | |
end; | |
contains_mapfield([_ | Rest]) -> | |
contains_mapfield(Rest); | |
contains_mapfield([]) -> | |
false. | |
msg_def_to_proto({{enum, Name}, EnumValues}) -> | |
f("enum ~s {~n" | |
"~s" | |
"}~n~n", | |
[Name, lists:map(fun({N,V}) -> f(" ~s = ~w;~n", [N, V]) end, | |
EnumValues)]); | |
msg_def_to_proto({{msg, Name}, Fields}) -> | |
f("message ~s {~n" | |
"~s" | |
"}~n~n", | |
[Name, lists:map(fun(#?gpb_field{}=F) -> field_to_proto(F, unless_map); | |
(#gpb_oneof{}=F) -> oneof_to_proto(F) | |
end, | |
Fields)]). | |
field_to_proto(#?gpb_field{name=FName, fnum=FNum, type=Type, opts=Opts, | |
occurrence=Occurrence}, ShowOccurrence) -> | |
Packed = lists:member(packed, Opts), | |
f(" ~s ~s ~s = ~w~s;~n", | |
[case {ShowOccurrence,Type} of | |
{unless_map,{map,_,_}} -> " "; | |
{unless_map,_} -> Occurrence; | |
{false,_} -> " " | |
end, | |
fmt_type(Type), | |
FName, | |
FNum, | |
if Packed -> " [packed=true]"; | |
not Packed -> "" | |
end]). | |
fmt_type({msg,Name2}) -> Name2; | |
fmt_type({enum,Name2}) -> Name2; | |
fmt_type({map,KT,VT}) -> f("map<~s,~s>", [fmt_type(KT),fmt_type(VT)]); | |
fmt_type(Type) -> Type. | |
oneof_to_proto(#gpb_oneof{name=FName, fields=OFields}) -> | |
f(" oneof ~s {~n" | |
"~s" | |
" };~n", | |
[FName, [field_to_proto(OField, false) || OField <- OFields]]). | |
decode_then_reencode_via_protoc(GpbBin, Msg, TmpDir) -> | |
ProtoFile = filename:join(TmpDir, "x.proto"), | |
ETxtFile = filename:join(TmpDir, "x.etxt"), | |
EMsgFile = filename:join(TmpDir, "x.emsg"), | |
PMsgFile = filename:join(TmpDir, "x.pmsg"), | |
TxtFile = filename:join(TmpDir, "x.txt"), | |
MsgName = element(1, Msg), | |
ok = file:write_file(ETxtFile, iolist_to_binary(f("~p~n", [Msg]))), | |
ok = file:write_file(EMsgFile, GpbBin), | |
Protoc = find_protoc(), | |
DRStr = os:cmd(f("'~s' --proto_path '~s'" | |
" --decode=~s '~s'" | |
" < '~s' > '~s'; echo $?~n", | |
[Protoc, TmpDir, MsgName, ProtoFile, EMsgFile, TxtFile])), | |
0 = list_to_integer(lib:nonl(DRStr)), | |
ERStr = os:cmd(f("'~s' --proto_path '~s'" | |
" --encode=~s '~s'" | |
" < '~s' > '~s'; echo $?~n", | |
[Protoc, TmpDir, MsgName, ProtoFile, TxtFile, PMsgFile])), | |
0 = list_to_integer(lib:nonl(ERStr)), | |
{ok, ProtoBin} = file:read_file(PMsgFile), | |
ProtoBin. | |
f(F,A) -> io_lib:format(F,A). | |
check_protoc_can_do_map_fields() -> | |
case find_protoc_version() of | |
{ok, Vsn} when Vsn >= [3,0] -> | |
true; | |
{ok, _} -> | |
false; | |
{error,Reason} -> | |
error(Reason) | |
end. | |
find_protoc_version() -> | |
Output = os:cmd(find_protoc() ++ " --version"), | |
case find_protoc_version_aux(string:tokens(Output, " \t\r\n"), Output) of | |
{ok, _}=Res -> Res; | |
{error, X}=Res -> | |
io:format(user,"Trouble finding protoc version in ~s~n", [X]), | |
Res | |
end. | |
find_protoc() -> | |
case os:getenv("PROTOC") of | |
false -> os:find_executable("protoc"); | |
Protoc -> Protoc | |
end. | |
find_protoc_version_aux(["libprotoc", VersionStr | _], All) -> | |
try {ok, [list_to_integer(X) || X <- string:tokens(VersionStr, ".")]} | |
catch error:badarg -> {error, {failed_to_interpret, VersionStr, All}} | |
end; | |
find_protoc_version_aux([_ | Rest], All) -> | |
find_protoc_version_aux(Rest, All); | |
find_protoc_version_aux([], All) -> | |
{error, {no_version_string_found, All}}. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment