blob: 2ac1695ace3d25f24337a1331ae61b3af4de47b1 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_ejson_compare_tests).
-define(MAX_UNICODE_STRING, <<255, 255, 255, 255>>).
% See mango_idx_view.hrl
-define(MAX_JSON_OBJ, {?MAX_UNICODE_STRING}).
-define(TEST_VALUES, [
null,
false,
true,
-2,
-0.1,
0,
0.1,
1,
2,
3.0,
4,
<<"a">>,
<<"A">>,
<<"aa">>,
<<"b">>,
<<"B">>,
<<"ba">>,
<<"bb">>,
% Highest sorting unicode value. Special case in the nif
?MAX_UNICODE_STRING,
[<<"a">>],
[<<"b">>],
[<<"b">>, <<"c">>],
[<<"b">>, <<"d">>],
[<<"b">>, <<"d">>, <<"e">>],
{[{<<"a">>, 1}]},
{[{<<"a">>, 2}]},
{[{<<"b">>, 1}]},
{[{<<"b">>, 2}]},
{[{<<"b">>, 2}, {<<"a">>, 1}]},
{[{<<"b">>, 2}, {<<"c">>, 2}]}
]).
-include_lib("eunit/include/eunit.hrl").
% Propery tests
-ifdef(WITH_PROPER).
-include_lib("couch/include/couch_eunit_proper.hrl").
property_test_() ->
?EUNIT_QUICKCHECK(60, 400).
% Properties
% The main, nif-based comparison, sorts the test values correctly
prop_nif_sorts_correctly() ->
Positions = get_positions(?TEST_VALUES),
?FORALL(
A,
oneof(?TEST_VALUES),
?FORALL(B, oneof(?TEST_VALUES), begin
expected_less(A, B, Positions) =:= less_nif(A, B)
end)
).
% The erlang fallback comparison sorts the test values correctly
prop_erlang_sorts_correctly() ->
Positions = get_positions(?TEST_VALUES),
?FORALL(
A,
oneof(?TEST_VALUES),
?FORALL(B, oneof(?TEST_VALUES), begin
expected_less(A, B, Positions) =:= less_erl(A, B)
end)
).
% Zero width unicode chars are ignored
prop_equivalent_unicode_values() ->
?FORALL({Prefix, Suffix}, {zero_width_list(), zero_width_list()}, begin
Binary = unicode:characters_to_binary(Prefix ++ [$a] ++ Suffix),
less(<<"a">>, Binary) =:= 0
end).
% Every test value sorts less than the special ?MAX_JSON_OBJ
prop_test_values_are_less_than_max_json() ->
?FORALL(V, oneof(?TEST_VALUES), begin
less(V, ?MAX_JSON_OBJ) =:= -1
end).
% Any json value sorts less than the special ?MAX_JSON_OBJ
prop_any_json_is_less_than_max_json() ->
?FORALL(V, json(), begin
less(V, ?MAX_JSON_OBJ) =:= -1
end).
% In general, for any json, the nif collator matches the erlang collator
prop_nif_matches_erlang() ->
?FORALL(
A,
json(),
?FORALL(B, json(), begin
less_nif(A, B) =:= less_erl(A, B)
end)
).
% Generators
json() ->
?SIZED(Size, json(Size)).
json(0) ->
oneof([
null,
true,
false,
json_number(),
json_string(),
[],
{[]}
]);
json(Size) ->
frequency([
{1, null},
{1, true},
{1, false},
{2, json_number()},
{3, json_string()},
{4, []},
{4, {[]}},
{5, ?LAZY(json_array(Size))},
{5, ?LAZY(json_object(Size))}
]).
json_number() ->
oneof([largeint(), int(), real()]).
json_string() ->
utf8().
json_array(0) ->
[];
json_array(Size) ->
vector(Size div 2, json(Size div 2)).
json_object(0) ->
{[]};
json_object(Size) ->
{vector(Size div 2, {json_string(), json(Size div 2)})}.
zero_width_list() ->
?SIZED(Size, vector(Size, zero_width_chars())).
zero_width_chars() ->
oneof([16#200B, 16#200C, 16#200D]).
-endif.
% Regular EUnit tests
get_icu_version_test() ->
Ver = couch_ejson_compare:get_icu_version(),
?assertMatch({_, _, _, _}, Ver),
{V1, V2, V3, V4} = Ver,
?assert(is_integer(V1) andalso V1 > 0),
?assert(is_integer(V2) andalso V2 >= 0),
?assert(is_integer(V3) andalso V3 >= 0),
?assert(is_integer(V4) andalso V4 >= 0).
get_uca_version_test() ->
Ver = couch_ejson_compare:get_uca_version(),
?assertMatch({_, _, _, _}, Ver),
{V1, V2, V3, V4} = Ver,
?assert(is_integer(V1) andalso V1 > 0),
?assert(is_integer(V2) andalso V2 >= 0),
?assert(is_integer(V3) andalso V3 >= 0),
?assert(is_integer(V4) andalso V4 >= 0).
get_collator_version_test() ->
Ver = couch_ejson_compare:get_collator_version(),
?assertMatch({_, _, _, _}, Ver),
{V1, V2, V3, V4} = Ver,
?assert(is_integer(V1) andalso V1 > 0),
?assert(is_integer(V2) andalso V2 >= 0),
?assert(is_integer(V3) andalso V3 >= 0),
?assert(is_integer(V4) andalso V4 >= 0).
max_depth_error_list_test() ->
% NIF can handle terms with depth <= 9
Nested9 = nest_list(<<"val">>, 9),
?assertEqual(0, less_nif(Nested9, Nested9)),
% At depth >= 10 it will throw a max_depth_error
Nested10 = nest_list(<<"val">>, 10),
?assertError(max_depth_error, less_nif(Nested10, Nested10)),
% Then it should transparently jump to erlang land
?assertEqual(0, less(Nested10, Nested10)).
max_depth_error_obj_test() ->
% NIF can handle terms with depth <= 9
Nested9 = nest_obj(<<"k">>, <<"v">>, 9),
?assertEqual(0, less_nif(Nested9, Nested9)),
% At depth >= 10 it will throw a max_depth_error
Nested10 = nest_obj(<<"k">>, <<"v">>, 10),
?assertError(max_depth_error, less_nif(Nested10, Nested10)),
% Then it should transparently jump to erlang land
?assertEqual(0, less(Nested10, Nested10)).
compare_strings_nif_test() ->
?assertEqual(-1, compare_strings(<<"a">>, <<"b">>)),
?assertEqual(0, compare_strings(<<"a">>, <<"a">>)),
?assertEqual(1, compare_strings(<<"b">>, <<"a">>)),
LargeBin1 = <<<<"x">> || _ <- lists:seq(1, 1000000)>>,
LargeBin2 = <<LargeBin1/binary, "x">>,
?assertEqual(-1, compare_strings(LargeBin1, LargeBin2)),
?assertEqual(1, compare_strings(LargeBin2, LargeBin1)),
?assertEqual(0, compare_strings(LargeBin1, LargeBin1)),
?assertError(badarg, compare_strings(42, <<"a">>)),
?assertError(badarg, compare_strings(<<"a">>, 42)),
?assertError(badarg, compare_strings(42, 42)).
% Helper functions
less(A, B) ->
cmp_norm(couch_ejson_compare:less(A, B)).
less_nif(A, B) ->
cmp_norm(couch_ejson_compare:less_nif(A, B)).
less_erl(A, B) ->
cmp_norm(couch_ejson_compare:less_erl(A, B)).
compare_strings(A, B) ->
couch_ejson_compare:compare_strings_nif(A, B).
nest_list(Val, 0) ->
Val;
nest_list(Val, Depth) when is_integer(Depth), Depth > 0 ->
[nest_list(Val, Depth - 1)].
nest_obj(K, V, 1) ->
{[{K, V}]};
nest_obj(K, V, Depth) when is_integer(Depth), Depth > 1 ->
{[{K, nest_obj(K, V, Depth - 1)}]}.
% Build a map of #{Val => PositionIndex} for the test values so that when any
% two are compared we can verify their position in the test list matches the
% compared result
get_positions(TestValues) ->
lists:foldl(
fun(Val, Acc) ->
Acc#{Val => map_size(Acc)}
end,
#{},
TestValues
).
% When two values are compared, check the test values positions index to ensure
% the order in the test value list matches the comparison result
expected_less(A, B, Positions) ->
#{A := PosA, B := PosB} = Positions,
if
PosA =:= PosB -> 0;
PosA < PosB -> -1;
PosA > PosB -> 1
end.
% Since collation functions can return magnitudes > 1, for example when
% comparing atoms A - B, we need to normalize the result to -1, 0, and 1.
cmp_norm(Cmp) when is_number(Cmp) ->
if
Cmp == 0 -> 0;
Cmp < 0 -> -1;
Cmp > 0 -> 1
end.