Support semver-style prerelease identifiers in version parsing

Use ec_semver from erlware for implementation. This is the same module
used in rebar3.

This changes some existing behaviour, e.g.:

3.0 is now minor-equivalent to 3.0.0 and 3.0.0.1
'master' is now a valid in version_compare

Add property tests

[#131650399]
This commit is contained in:
Andrew Bruce 2016-10-12 16:37:21 +01:00
parent acbb153054
commit 64238b139d
3 changed files with 185 additions and 47 deletions

View File

@ -1,7 +1,7 @@
PROJECT = rabbit_common
BUILD_DEPS = rabbitmq_codegen
TEST_DEPS = mochiweb
TEST_DEPS = mochiweb proper
.DEFAULT_GOAL = all

View File

@ -737,52 +737,27 @@ compose_pid(Node, Cre, Id, Ser) ->
<<131,NodeEnc/binary>> = term_to_binary(Node),
binary_to_term(<<131,103,NodeEnc/binary,Id:32,Ser:32,Cre:8>>).
version_compare(A, B, lte) ->
case version_compare(A, B) of
eq -> true;
lt -> true;
gt -> false
end;
version_compare(A, B, gte) ->
case version_compare(A, B) of
eq -> true;
gt -> true;
lt -> false
end;
version_compare(A, B, Result) ->
Result =:= version_compare(A, B).
version_compare(A, B, eq) -> ec_semver:eql(A, B);
version_compare(A, B, lt) -> ec_semver:lt(A, B);
version_compare(A, B, lte) -> ec_semver:lte(A, B);
version_compare(A, B, gt) -> ec_semver:gt(A, B);
version_compare(A, B, gte) -> ec_semver:gte(A, B).
version_compare(A, A) ->
eq;
version_compare([], [$0 | B]) ->
version_compare([], dropdot(B));
version_compare([], _) ->
lt; %% 2.3 < 2.3.1
version_compare([$0 | A], []) ->
version_compare(dropdot(A), []);
version_compare(_, []) ->
gt; %% 2.3.1 > 2.3
version_compare(A, B) ->
{AStr, ATl} = lists:splitwith(fun (X) -> X =/= $. end, A),
{BStr, BTl} = lists:splitwith(fun (X) -> X =/= $. end, B),
ANum = list_to_integer(AStr),
BNum = list_to_integer(BStr),
if ANum =:= BNum -> version_compare(dropdot(ATl), dropdot(BTl));
ANum < BNum -> lt;
ANum > BNum -> gt
version_compare(A, B) ->
case version_compare(A, B, lt) of
true -> lt;
false -> case version_compare(A, B, gt) of
true -> gt;
false -> eq
end
end.
%% a.b.c and a.b.d match, but a.b.c and a.d.e don't. If
%% versions do not match that pattern, just compare them.
version_minor_equivalent(A, B) ->
{ok, RE} = re:compile("^(\\d+\\.\\d+)(\\.\\d+)\$"),
Opts = [{capture, all_but_first, list}],
case {re:run(A, RE, Opts), re:run(B, RE, Opts)} of
{{match, [A1|_]}, {match, [B1|_]}} -> A1 =:= B1;
_ -> A =:= B
end.
dropdot(A) -> lists:dropwhile(fun (X) -> X =:= $. end, A).
{{MajA, MinA, _, _}, _} = ec_semver:normalize(ec_semver:parse(A)),
{{MajB, MinB, _, _}, _} = ec_semver:normalize(ec_semver:parse(B)),
MajA =:= MajB andalso MinA =:= MinB.
dict_cons(Key, Value, Dict) ->
dict:update(Key, fun (List) -> [Value | List] end, [Value], Dict).

View File

@ -17,6 +17,7 @@
-module(unit_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("proper/include/proper.hrl").
-compile(export_all).
@ -28,7 +29,9 @@ all() ->
groups() ->
[
{parallel_tests, [parallel], [
version_equivalence
version_equivalence,
version_minor_equivalence_properties,
version_comparison
]}
].
@ -39,8 +42,168 @@ version_equivalence(_Config) ->
true = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.0"),
true = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.1"),
true = rabbit_misc:version_minor_equivalent("%%VSN%%", "%%VSN%%"),
false = rabbit_misc:version_minor_equivalent("3.0.0", "3.1.0"),
false = rabbit_misc:version_minor_equivalent("3.0.0", "3.0"),
false = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.0.1"),
false = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.foo"),
passed.
true = rabbit_misc:version_minor_equivalent("3.0.0", "3.0"),
true = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.0.1"),
true = rabbit_misc:version_minor_equivalent("3.0.0", "3.0.foo"),
false = rabbit_misc:version_minor_equivalent("3.0.0", "3.1.0").
version_minor_equivalence_properties(_Config) ->
true = proper:counterexample(
?FORALL(
{A, B},
{version(), version()},
check_minor_equivalent(A, B)
),
[
quiet,
{numtests, 10000},
{on_output, fun(F, A) -> ct:pal(?LOW_IMPORTANCE, F, A) end}
]
).
version_comparison(_Config) ->
true = proper:counterexample(
?FORALL(
{A, B},
{version(), version()},
check_and_compare_versions(A, B)
),
[
quiet,
{numtests, 10000},
{on_output, fun(F, A) -> ct:pal(?LOW_IMPORTANCE, F, A) end}
]
).
version() ->
union([
[],
release(),
prerelease()
]).
release() ->
union([
identifier(),
[non_neg_integer()],
[non_neg_integer(), ".", 0],
[non_neg_integer(), ".", frequency([{1, 0}, {1, pos_integer()}])],
[non_neg_integer(), ".", non_neg_integer(), ".", frequency([{1, 0}, {1, pos_integer()}])]
]).
prerelease() ->
{release(), "-", identifier()}.
identifier() ->
union(
[[identifier_first_char()],
non_empty(list(identifier_char()))]
).
identifier_first_char() ->
union([non_zero_digit(), uppercase(), lowercase()]).
%% FIXME: We should have $- as a valid identifier_char(), but the
%% ec_semver library doesn't support having a dash as the last
%% character in an identifier. For now, do not use dashes in an
%% identifier. We could probably fix the property to only generate dash
%% as the non-first non-last character.
identifier_char() ->
union([digit(), uppercase(), lowercase()]).
digit() -> integer(48, 57).
non_zero_digit() -> integer(49, 57).
uppercase() -> integer(65, 90).
lowercase() -> integer(97, 122).
check_minor_equivalent({Release, Sep, Extra}, B) ->
A = Release ++ [Sep, Extra],
check_minor_equivalent(A, B);
check_minor_equivalent(A, {Release, Sep, Extra}) ->
B = Release ++ [Sep, Extra],
check_minor_equivalent(A, B);
check_minor_equivalent([], []) ->
check_minor_equivalent([], [], true);
check_minor_equivalent([Maj, ".", 0 | _] = A, [Maj] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, ".", 0 | _] = A, [Maj, "-", _ | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj] = A, [Maj, ".", 0 | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, "-", _ | _] = A, [Maj, ".", 0 | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, ".", 0 | _] = A, [Maj, ".", 0 | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj] = A, [Maj] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, "-", _ | _] = A, [Maj] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj] = A, [Maj, "-", _ | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, "-", _ | _] = A, [Maj, "-", _ | _] = B)
when is_integer(Maj) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent([Maj, ".", Min | _] = A, [Maj, ".", Min | _] = B)
when is_integer(Maj) andalso is_integer(Min) ->
check_minor_equivalent(A, B, true);
check_minor_equivalent(A, B) ->
check_minor_equivalent(A, B, false).
check_minor_equivalent(RawA, RawB, Expected) ->
A = lists:flatten([raw_to_string(Char) || Char <- RawA]),
B = lists:flatten([raw_to_string(Char) || Char <- RawB]),
Expected =:= rabbit_misc:version_minor_equivalent(A, B).
check_and_compare_versions({Release, Sep, Extra}, B) ->
A = Release ++ [Sep, Extra],
check_and_compare_versions(A, B);
check_and_compare_versions(A, {Release, Sep, Extra}) ->
B = Release ++ [Sep, Extra],
check_and_compare_versions(A, B);
check_and_compare_versions(RawA, RawB) ->
A = lists:flatten([raw_to_string(Char) || Char <- RawA]),
B = lists:flatten([raw_to_string(Char) || Char <- RawB]),
Result1 = rabbit_misc:version_compare(A, B),
Result2 = rabbit_misc:version_compare(B, A),
case {Result1, Result2} of
{lt, gt} ->
true =:= rabbit_misc:version_compare(A, B, lte) andalso
false =:= rabbit_misc:version_compare(A, B, gte) andalso
false =:= rabbit_misc:version_compare(A, B, eq);
{gt, lt} ->
true =:= rabbit_misc:version_compare(A, B, gte) andalso
false =:= rabbit_misc:version_compare(A, B, lte) andalso
false =:= rabbit_misc:version_compare(A, B, eq);
{eq, eq} ->
true =:= rabbit_misc:version_compare(A, B, gte) andalso
true =:= rabbit_misc:version_compare(A, B, lte) andalso
true =:= rabbit_misc:version_compare(A, B, eq);
_ ->
ct:pal(
"rabbit_misc:version_compare/2 failure:~n"
"A: ~p~n"
"B: ~p~n"
"Result1: ~p~n"
"Result2: ~p~n", [A, B, Result1, Result2]),
false
end.
raw_to_string(Char)
when is_integer(Char) ->
integer_to_list(Char);
raw_to_string(Char) ->
Char.