fix(variform): add basic tests
This commit is contained in:
parent
0e79b543cf
commit
bf12efac6d
|
@ -1,5 +1,5 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%%
|
%%
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
%% you may not use this file except in compliance with the License.
|
%% you may not use this file except in compliance with the License.
|
||||||
|
@ -22,7 +22,12 @@
|
||||||
%% or used to choose the first non-empty value from a list of variables.
|
%% or used to choose the first non-empty value from a list of variables.
|
||||||
-module(emqx_variform).
|
-module(emqx_variform).
|
||||||
|
|
||||||
-export([inject_allowed_modules/1]).
|
-export([
|
||||||
|
inject_allowed_module/1,
|
||||||
|
inject_allowed_modules/1,
|
||||||
|
erase_allowed_module/1,
|
||||||
|
erase_allowed_modules/1
|
||||||
|
]).
|
||||||
-export([render/2, render/3]).
|
-export([render/2, render/3]).
|
||||||
|
|
||||||
%% @doc Render a variform expression with bindings.
|
%% @doc Render a variform expression with bindings.
|
||||||
|
@ -48,6 +53,8 @@
|
||||||
render(Expression, Bindings) ->
|
render(Expression, Bindings) ->
|
||||||
render(Expression, Bindings, #{}).
|
render(Expression, Bindings, #{}).
|
||||||
|
|
||||||
|
render(Expression, Bindings, Opts) when is_binary(Expression) ->
|
||||||
|
render(unicode:characters_to_list(Expression), Bindings, Opts);
|
||||||
render(Expression, Bindings, Opts) ->
|
render(Expression, Bindings, Opts) ->
|
||||||
case emqx_variform_scan:string(Expression) of
|
case emqx_variform_scan:string(Expression) of
|
||||||
{ok, Tokens, _Line} ->
|
{ok, Tokens, _Line} ->
|
||||||
|
@ -66,7 +73,7 @@ render(Expression, Bindings, Opts) ->
|
||||||
|
|
||||||
eval_as_string(Expr, Bindings, _Opts) ->
|
eval_as_string(Expr, Bindings, _Opts) ->
|
||||||
try
|
try
|
||||||
{ok, iolist_to_binary(eval(Expr, Bindings))}
|
{ok, str(eval(Expr, Bindings))}
|
||||||
catch
|
catch
|
||||||
throw:Reason ->
|
throw:Reason ->
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
|
@ -97,7 +104,7 @@ call(emqx_variform_str, concat, Args) ->
|
||||||
call(emqx_variform_str, coalesce, Args) ->
|
call(emqx_variform_str, coalesce, Args) ->
|
||||||
str(emqx_variform_str:coalesce(Args));
|
str(emqx_variform_str:coalesce(Args));
|
||||||
call(Mod, Fun, Args) ->
|
call(Mod, Fun, Args) ->
|
||||||
str(erlang:apply(Mod, Fun, Args)).
|
erlang:apply(Mod, Fun, Args).
|
||||||
|
|
||||||
resolve_func_name(FuncNameStr) ->
|
resolve_func_name(FuncNameStr) ->
|
||||||
case string:tokens(FuncNameStr, ".") of
|
case string:tokens(FuncNameStr, ".") of
|
||||||
|
@ -107,7 +114,10 @@ resolve_func_name(FuncNameStr) ->
|
||||||
list_to_existing_atom(Mod0)
|
list_to_existing_atom(Mod0)
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
throw(#{unknown_module => Mod0})
|
throw(#{
|
||||||
|
reason => unknown_variform_module,
|
||||||
|
module => Mod0
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
ok = assert_module_allowed(Mod),
|
ok = assert_module_allowed(Mod),
|
||||||
Fun =
|
Fun =
|
||||||
|
@ -115,7 +125,10 @@ resolve_func_name(FuncNameStr) ->
|
||||||
list_to_existing_atom(Fun0)
|
list_to_existing_atom(Fun0)
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
throw(#{unknown_function => Fun0})
|
throw(#{
|
||||||
|
reason => unknown_variform_function,
|
||||||
|
function => Fun0
|
||||||
|
})
|
||||||
end,
|
end,
|
||||||
{Mod, Fun};
|
{Mod, Fun};
|
||||||
[Fun] ->
|
[Fun] ->
|
||||||
|
@ -125,11 +138,13 @@ resolve_func_name(FuncNameStr) ->
|
||||||
catch
|
catch
|
||||||
error:badarg ->
|
error:badarg ->
|
||||||
throw(#{
|
throw(#{
|
||||||
reason => "unknown_variform_function",
|
reason => unknown_variform_function,
|
||||||
function => Fun
|
function => Fun
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
{emqx_variform_str, FuncName}
|
{emqx_variform_str, FuncName};
|
||||||
|
_ ->
|
||||||
|
throw(#{reason => invalid_function_reference, function => FuncNameStr})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
resolve_var_value(VarName, Bindings) ->
|
resolve_var_value(VarName, Bindings) ->
|
||||||
|
@ -145,13 +160,14 @@ assert_func_exported(emqx_variform_str, concat, _Arity) ->
|
||||||
assert_func_exported(emqx_variform_str, coalesce, _Arity) ->
|
assert_func_exported(emqx_variform_str, coalesce, _Arity) ->
|
||||||
ok;
|
ok;
|
||||||
assert_func_exported(Mod, Fun, Arity) ->
|
assert_func_exported(Mod, Fun, Arity) ->
|
||||||
|
%% ensure beam loaded
|
||||||
_ = Mod:module_info(md5),
|
_ = Mod:module_info(md5),
|
||||||
case erlang:function_exported(Mod, Fun, Arity) of
|
case erlang:function_exported(Mod, Fun, Arity) of
|
||||||
true ->
|
true ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
throw(#{
|
throw(#{
|
||||||
reason => "unknown_variform_function",
|
reason => unknown_variform_function,
|
||||||
module => Mod,
|
module => Mod,
|
||||||
function => Fun,
|
function => Fun,
|
||||||
arity => Arity
|
arity => Arity
|
||||||
|
@ -167,16 +183,27 @@ assert_module_allowed(Mod) ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
throw(#{
|
throw(#{
|
||||||
reason => "unallowed_veriform_module",
|
reason => unallowed_veriform_module,
|
||||||
module => Mod
|
module => Mod
|
||||||
})
|
})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
inject_allowed_modules(Modules) ->
|
inject_allowed_module(Module) when is_atom(Module) ->
|
||||||
|
inject_allowed_modules([Module]).
|
||||||
|
|
||||||
|
inject_allowed_modules(Modules) when is_list(Modules) ->
|
||||||
Allowed0 = get_allowed_modules(),
|
Allowed0 = get_allowed_modules(),
|
||||||
Allowed = lists:usort(Allowed0 ++ Modules),
|
Allowed = lists:usort(Allowed0 ++ Modules),
|
||||||
persistent_term:put({emqx_variform, allowed_modules}, Allowed).
|
persistent_term:put({emqx_variform, allowed_modules}, Allowed).
|
||||||
|
|
||||||
|
erase_allowed_module(Module) when is_atom(Module) ->
|
||||||
|
erase_allowed_modules([Module]).
|
||||||
|
|
||||||
|
erase_allowed_modules(Modules) when is_list(Modules) ->
|
||||||
|
Allowed0 = get_allowed_modules(),
|
||||||
|
Allowed = Allowed0 -- Modules,
|
||||||
|
persistent_term:put({emqx_variform, allowed_modules}, Allowed).
|
||||||
|
|
||||||
get_allowed_modules() ->
|
get_allowed_modules() ->
|
||||||
persistent_term:get({emqx_variform, allowed_modules}, []).
|
persistent_term:get({emqx_variform, allowed_modules}, []).
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,8 @@
|
||||||
find/3,
|
find/3,
|
||||||
join_to_string/1,
|
join_to_string/1,
|
||||||
join_to_string/2,
|
join_to_string/2,
|
||||||
unescape/1
|
unescape/1,
|
||||||
|
nth/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)).
|
-define(IS_EMPTY(X), (X =:= <<>> orelse X =:= "" orelse X =:= undefined)).
|
||||||
|
@ -224,6 +225,20 @@ unescape(Bin) when is_binary(Bin) ->
|
||||||
throw({invalid_unicode_character, Error})
|
throw({invalid_unicode_character, Error})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
nth(N, List) when (is_list(N) orelse is_binary(N)) andalso is_list(List) ->
|
||||||
|
try binary_to_integer(iolist_to_binary(N)) of
|
||||||
|
N1 ->
|
||||||
|
nth(N1, List)
|
||||||
|
catch
|
||||||
|
_:_ ->
|
||||||
|
throw(#{reason => invalid_argument, func => nth, index => N})
|
||||||
|
end;
|
||||||
|
nth(N, List) when is_integer(N) andalso is_list(List) ->
|
||||||
|
case length(List) of
|
||||||
|
L when L < N -> <<>>;
|
||||||
|
_ -> lists:nth(N, List)
|
||||||
|
end.
|
||||||
|
|
||||||
unescape_string(Input) -> unescape_string(Input, []).
|
unescape_string(Input) -> unescape_string(Input, []).
|
||||||
|
|
||||||
unescape_string([], Acc) ->
|
unescape_string([], Acc) ->
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% 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(emqx_variform_tests).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(SYNTAX_ERROR, {error, "syntax error before:" ++ _}).
|
||||||
|
|
||||||
|
redner_test_() ->
|
||||||
|
[
|
||||||
|
{"direct var reference", fun() -> ?assertEqual({ok, <<"1">>}, render("a", #{a => 1})) end},
|
||||||
|
{"concat strings", fun() ->
|
||||||
|
?assertEqual({ok, <<"a,b">>}, render("concat('a',',','b')", #{}))
|
||||||
|
end},
|
||||||
|
{"concat empty string", fun() -> ?assertEqual({ok, <<"">>}, render("concat('')", #{})) end},
|
||||||
|
{"tokens 1st", fun() ->
|
||||||
|
?assertEqual({ok, <<"a">>}, render("nth(1,tokens(var, ','))", #{var => <<"a,b">>}))
|
||||||
|
end},
|
||||||
|
{"unknown var as empty str", fun() ->
|
||||||
|
?assertEqual({ok, <<>>}, render("var", #{}))
|
||||||
|
end},
|
||||||
|
{"out of range nth index", fun() ->
|
||||||
|
?assertEqual({ok, <<>>}, render("nth(2, tokens(var, ','))", #{var => <<"a">>}))
|
||||||
|
end},
|
||||||
|
{"not a index number for nth", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := invalid_argument, func := nth, index := <<"notnum">>}},
|
||||||
|
render("nth('notnum', tokens(var, ','))", #{var => <<"a">>})
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
unknown_func_test_() ->
|
||||||
|
[
|
||||||
|
{"unknown function", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := unknown_variform_function}},
|
||||||
|
render("nonexistingatom__(a)", #{})
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"unknown module", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := unknown_variform_module}},
|
||||||
|
render("nonexistingatom__.nonexistingatom__(a)", #{})
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"unknown function in a known module", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := unknown_variform_function}},
|
||||||
|
render("emqx_variform_str.nonexistingatom__(a)", #{})
|
||||||
|
)
|
||||||
|
end},
|
||||||
|
{"invalid func reference", fun() ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := invalid_function_reference, function := "a.b.c"}},
|
||||||
|
render("a.b.c(var)", #{})
|
||||||
|
)
|
||||||
|
end}
|
||||||
|
].
|
||||||
|
|
||||||
|
concat(L) -> iolist_to_binary(L).
|
||||||
|
|
||||||
|
inject_allowed_module_test() ->
|
||||||
|
try
|
||||||
|
emqx_variform:inject_allowed_module(?MODULE),
|
||||||
|
?assertEqual({ok, <<"ab">>}, render(atom_to_list(?MODULE) ++ ".concat(['a','b'])", #{})),
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{
|
||||||
|
reason := unknown_variform_function,
|
||||||
|
module := ?MODULE,
|
||||||
|
function := concat,
|
||||||
|
arity := 2
|
||||||
|
}},
|
||||||
|
render(atom_to_list(?MODULE) ++ ".concat('a','b')", #{})
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{reason := unallowed_veriform_module, module := emqx}},
|
||||||
|
render("emqx.concat('a','b')", #{})
|
||||||
|
)
|
||||||
|
after
|
||||||
|
emqx_variform:erase_allowed_module(?MODULE)
|
||||||
|
end.
|
||||||
|
|
||||||
|
coalesce_test_() ->
|
||||||
|
[
|
||||||
|
{"coalesce first", fun() ->
|
||||||
|
?assertEqual({ok, <<"a">>}, render("coalesce('a','b')", #{}))
|
||||||
|
end},
|
||||||
|
{"coalesce second", fun() ->
|
||||||
|
?assertEqual({ok, <<"b">>}, render("coalesce('', 'b')", #{}))
|
||||||
|
end},
|
||||||
|
{"coalesce first var", fun() ->
|
||||||
|
?assertEqual({ok, <<"a">>}, render("coalesce(a,b)", #{a => <<"a">>, b => <<"b">>}))
|
||||||
|
end},
|
||||||
|
{"coalesce second var", fun() ->
|
||||||
|
?assertEqual({ok, <<"b">>}, render("coalesce(a,b)", #{b => <<"b">>}))
|
||||||
|
end},
|
||||||
|
{"coalesce empty", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(a,b)", #{})) end}
|
||||||
|
].
|
||||||
|
|
||||||
|
syntax_error_test_() ->
|
||||||
|
[
|
||||||
|
{"empty expression", fun() -> ?assertMatch(?SYNTAX_ERROR, render("", #{})) end},
|
||||||
|
{"const string single quote", fun() -> ?assertMatch(?SYNTAX_ERROR, render("'a'", #{})) end},
|
||||||
|
{"const string double quote", fun() ->
|
||||||
|
?assertMatch(?SYNTAX_ERROR, render(<<"\"a\"">>, #{}))
|
||||||
|
end},
|
||||||
|
{"no arity", fun() -> ?assertMatch(?SYNTAX_ERROR, render("concat()", #{})) end}
|
||||||
|
].
|
||||||
|
|
||||||
|
render(Expression, Bindings) ->
|
||||||
|
emqx_variform:render(Expression, Bindings).
|
Loading…
Reference in New Issue