blob: b518028cc625db144bd43216fb980dad84f5619e [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_util_tests).
-include_lib("couch/include/couch_eunit.hrl").
% For generating poisson distributed string lengths
% in the random unicode generation. This shoots
% for lengths centered around 24 characters. To
% change, replace this value with math:exp(-Length).
-define(POISSON_LIMIT, 3.775134544279098e-11).
-define(RANDOM_TEST_SIZE, 10000).
setup() ->
%% We cannot start driver from here since it becomes bounded to eunit
%% master process and the next couch_server_sup:start_link call will
%% fail because server couldn't load driver since it already is.
%%
%% On other hand, we cannot unload driver here due to
%% {error, not_loaded_by_this_process} while it is. Any ideas is welcome.
%%
Ctx = test_util:start_couch(),
%% config:start_link(?CONFIG_CHAIN),
%% {ok, _} = couch_drv:start_link(),
Ctx.
teardown(Ctx) ->
ok = test_util:stop_couch(Ctx),
%% config:stop(),
%% erl_ddll:unload_driver(couch_icu_driver),
ok.
collation_test_() ->
{
"Collation tests",
[
{
setup,
fun setup/0, fun teardown/1,
[
should_collate_ascii(),
should_collate_non_ascii()
]
}
]
}.
validate_callback_exists_test_() ->
{
"validate_callback_exists tests",
[
fun should_succeed_for_existent_cb/0,
should_fail_for_missing_cb()
]
}.
should_collate_ascii() ->
?_assertEqual(1, couch_util:collate(<<"foo">>, <<"bar">>)).
should_collate_non_ascii() ->
?_assertEqual(-1, couch_util:collate(<<"A">>, <<"aa">>)).
to_existed_atom_test() ->
?assert(couch_util:to_existing_atom(true)),
?assertMatch(foo, couch_util:to_existing_atom(<<"foo">>)),
?assertMatch(foobarbaz, couch_util:to_existing_atom("foobarbaz")).
implode_test() ->
?assertEqual([1, 38, 2, 38, 3], couch_util:implode([1, 2, 3], "&")).
trim_test() ->
lists:map(fun(S) -> ?assertEqual("foo", couch_util:trim(S)) end,
[" foo", "foo ", "\tfoo", " foo ", "foo\t", "foo\n", "\nfoo"]).
abs_pathname_test() ->
{ok, Cwd} = file:get_cwd(),
?assertEqual(Cwd ++ "/foo", couch_util:abs_pathname("./foo")).
flush_test() ->
?assertNot(couch_util:should_flush()),
AcquireMem = fun() ->
_IntsToAGazillion = lists:seq(1, 200000),
_LotsOfData = lists:map(fun(_) -> <<"foobar">> end,
lists:seq(1, 500000)),
_ = list_to_binary(_LotsOfData),
%% Allocation 200K tuples puts us above the memory threshold
%% Originally, there should be:
%% ?assertNot(should_flush())
%% however, unlike for etap test, GC collects all allocated bits
%% making this conditions fail. So we have to invert the condition
%% since GC works, cleans the memory and everything is fine.
?assertNot(couch_util:should_flush())
end,
AcquireMem(),
%% Checking to flush invokes GC
?assertNot(couch_util:should_flush()).
verify_test() ->
?assert(couch_util:verify("It4Vooya", "It4Vooya")),
?assertNot(couch_util:verify("It4VooyaX", "It4Vooya")),
?assert(couch_util:verify(<<"ahBase3r">>, <<"ahBase3r">>)),
?assertNot(couch_util:verify(<<"ahBase3rX">>, <<"ahBase3r">>)),
?assertNot(couch_util:verify(nil, <<"ahBase3r">>)).
find_in_binary_test_() ->
Cases = [
{<<"foo">>, <<"foobar">>, {exact, 0}},
{<<"foo">>, <<"foofoo">>, {exact, 0}},
{<<"foo">>, <<"barfoo">>, {exact, 3}},
{<<"foo">>, <<"barfo">>, {partial, 3}},
{<<"f">>, <<"fobarfff">>, {exact, 0}},
{<<"f">>, <<"obarfff">>, {exact, 4}},
{<<"f">>, <<"obarggf">>, {exact, 6}},
{<<"f">>, <<"f">>, {exact, 0}},
{<<"f">>, <<"g">>, not_found},
{<<"foo">>, <<"f">>, {partial, 0}},
{<<"foo">>, <<"g">>, not_found},
{<<"foo">>, <<"">>, not_found},
{<<"fofo">>, <<"foofo">>, {partial, 3}},
{<<"foo">>, <<"gfobarfo">>, {partial, 6}},
{<<"foo">>, <<"gfobarf">>, {partial, 6}},
{<<"foo">>, <<"gfobar">>, not_found},
{<<"fog">>, <<"gbarfogquiz">>, {exact, 4}},
{<<"ggg">>, <<"ggg">>, {exact, 0}},
{<<"ggg">>, <<"ggggg">>, {exact, 0}},
{<<"ggg">>, <<"bggg">>, {exact, 1}},
{<<"ggg">>, <<"bbgg">>, {partial, 2}},
{<<"ggg">>, <<"bbbg">>, {partial, 3}},
{<<"ggg">>, <<"bgbggbggg">>, {exact, 6}},
{<<"ggg">>, <<"bgbggb">>, not_found}
],
lists:map(
fun({Needle, Haystack, Result}) ->
Msg = lists:flatten(io_lib:format("Looking for ~s in ~s",
[Needle, Haystack])),
{Msg, ?_assertMatch(Result,
couch_util:find_in_binary(Needle, Haystack))}
end, Cases).
should_succeed_for_existent_cb() ->
?_assert(couch_util:validate_callback_exists(lists, any, 2)).
should_fail_for_missing_cb() ->
Cases = [
{unknown_module, any, 1},
{erlang, unknown_function, 1},
{erlang, whereis, 100}
],
lists:map(
fun({M, F, A} = MFA) ->
Name = lists:flatten(io_lib:format("~w:~w/~w", [M, F, A])),
{Name, ?_assertThrow(
{error, {undefined_callback, Name, MFA}},
couch_util:validate_callback_exists(M, F, A))}
end, Cases).
to_hex_test_() ->
[
?_assertEqual("", couch_util:to_hex([])),
?_assertEqual("010203faff", couch_util:to_hex([1, 2, 3, 250, 255])),
?_assertEqual("", couch_util:to_hex(<<>>)),
?_assertEqual("010203faff", couch_util:to_hex(<<1, 2, 3, 250, 255>>))
].
sort_key_test_() ->
{
"Sort Key tests",
[
{
foreach,
fun setup/0, fun teardown/1,
[
fun test_get_sort_key/1,
fun test_get_sort_key_jiffy_string/1,
fun test_get_sort_key_fails_on_bad_input/1,
fun test_get_sort_key_longer_than_buffer/1,
fun test_sort_key_collation/1,
fun test_sort_key_list_sort/1
]
}
]
}.
test_get_sort_key(_) ->
Strs = [
<<"">>,
<<"foo">>,
<<"bar">>,
<<"Bar">>,
<<"baz">>,
<<"BAZ">>,
<<"quaz">>,
<<"1234fdsa">>,
<<"1234">>,
<<"pizza">>
],
Pairs = [{S1, S2} || S1 <- Strs, S2 <- Strs],
lists:map(fun({S1, S2}) ->
S1K = couch_util:get_sort_key(S1),
S2K = couch_util:get_sort_key(S2),
SortRes = sort_keys(S1K, S2K),
Comment = list_to_binary(io_lib:format("strcmp(~p, ~p)", [S1, S2])),
CollRes = couch_util:collate(S1, S2),
{Comment, ?_assertEqual(SortRes, CollRes)}
end, Pairs).
test_get_sort_key_jiffy_string(_) ->
%% jiffy:decode does not null terminate strings
%% so we use it here to test unterminated strings
{[{S1,S2}]} = jiffy:decode(<<"{\"foo\": \"bar\"}">>),
S1K = couch_util:get_sort_key(S1),
S2K = couch_util:get_sort_key(S2),
SortRes = sort_keys(S1K, S2K),
CollRes = couch_util:collate(S1, S2),
?_assertEqual(SortRes, CollRes).
test_get_sort_key_fails_on_bad_input(_) ->
%% generated with crypto:strong_rand_bytes
%% contains invalid character, should error
S = <<209,98,222,144,60,163,72,134,206,157>>,
Res = couch_util:get_sort_key(S),
?_assertEqual(error, Res).
test_get_sort_key_longer_than_buffer(_) ->
%% stack allocated buffer is 1024 units
%% test resize logic with strings > 1024 char
Extra = list_to_binary(["a" || _ <- lists:seq(1, 1200)]),
?_assert(is_binary(Extra)).
test_sort_key_collation(_) ->
?_test(begin
lists:foreach(fun(_) ->
K1 = random_unicode_binary(),
SK1 = couch_util:get_sort_key(K1),
K2 = random_unicode_binary(),
SK2 = couch_util:get_sort_key(K2),
% Probably kinda silly but whatevs
?assertEqual(couch_util:collate(K1, K1), sort_keys(SK1, SK1)),
?assertEqual(couch_util:collate(K2, K2), sort_keys(SK2, SK2)),
?assertEqual(couch_util:collate(K1, K2), sort_keys(SK1, SK2)),
?assertEqual(couch_util:collate(K2, K1), sort_keys(SK2, SK1))
end, lists:seq(1, ?RANDOM_TEST_SIZE))
end).
test_sort_key_list_sort(_) ->
?_test(begin
RandomKeys = lists:map(fun(_) ->
random_unicode_binary()
end, lists:seq(1, ?RANDOM_TEST_SIZE)),
CollationSorted = lists:sort(fun(A, B) ->
couch_util:collate(A, B) =< 0
end, RandomKeys),
SortKeys = lists:map(fun(K) ->
{couch_util:get_sort_key(K), K}
end, RandomKeys),
{_, SortKeySorted} = lists:unzip(lists:sort(SortKeys)),
?assertEqual(CollationSorted, SortKeySorted)
end).
sort_keys(S1, S2) ->
case S1 < S2 of
true ->
-1;
false -> case S1 =:= S2 of
true ->
0;
false ->
1
end
end.
random_unicode_binary() ->
Size = poisson_length(0, rand:uniform()),
Chars = [random_unicode_char() || _ <- lists:seq(1, Size)],
<<_/binary>> = unicode:characters_to_binary(Chars).
poisson_length(N, Acc) when Acc > ?POISSON_LIMIT ->
poisson_length(N + 1, Acc * rand:uniform());
poisson_length(N, _) ->
N.
random_unicode_char() ->
BaseChar = rand:uniform(16#FFFD + 1) - 1,
case BaseChar of
BC when BC >= 16#D800, BC =< 16#DFFF ->
% This range is reserved for surrogate pair
% encodings.
random_unicode_char();
BC ->
BC
end.