%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2003-2009. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %%% @doc Common Test Framework Utilities. %%% %%%
This is a support module for the Common Test Framework. It %%% implements the process ct_util_server which acts like a data %%% holder for suite, configuration and connection data.
%%% -module(ct_util). -export([start/0,start/1,start/2,stop/1,update_last_run_index/0]). -export([register_connection/4,unregister_connection/1, does_connection_exist/3,get_key_from_name/1]). -export([require/1, require/2, get_config/1, get_config/2, get_config/3, set_default_config/2, set_default_config/3, delete_default_config/1, get_all_config/0, update_config/2, release_allocated/0, close_connections/0]). -export([save_suite_data/3, save_suite_data/2, read_suite_data/1, delete_suite_data/0, delete_suite_data/1, match_delete_suite_data/1, delete_testdata/0, delete_testdata/1, set_testdata/1, get_testdata/1, update_testdata/2]). -export([override_silence_all_connections/0, override_silence_connections/1, get_overridden_silenced_connections/0, delete_overridden_silenced_connections/0, silence_all_connections/0, silence_connections/1, is_silenced/1, reset_silent_connections/0]). -export([set_cwd/1, reset_cwd/0]). -export([parse_table/1]). -export([listenv/1]). -export([get_target_name/1, get_connections/2]). -export([is_test_dir/1, get_testdir/2]). -export([encrypt_config_file/2, encrypt_config_file/3, decrypt_config_file/2, decrypt_config_file/3]). -export([kill_attached/2, get_attached/1]). -export([warn_duplicates/1]). -include("ct_event.hrl"). -include("ct_util.hrl"). -record(ct_conf,{key,value,ref,name='_UNDEF',default=false}). %% default = {true,suite} | {true,testcase} | false -record(suite_data, {key,name,value}). -define(cryptfile, ".ct_config.crypt"). %%%----------------------------------------------------------------- %%% @spec start(Mode) -> Pid | exit(Error) %%% Mode = normal | interactive %%% Pid = pid() %%% %%% @doc Start start the ct_util_server process %%% (tool-internal use only). %%% %%%This function is called from ct_run.erl. It starts and initiates
%%% the ct_util_server
Returns the process identity of the
%%% ct_util_server
.
This function can be called when a new connection is
%%% established. The connection data is stored in the connection
%%% table, and ct_util will close all registered connections when the
%%% test is finished by calling Callback:close/1
.
This function should be called when a registered connection is %%% closed. It removes the connection data from the connection %%% table.
unregister_connection(Handle) -> ets:delete(?conn_table,Handle), ok. %%%----------------------------------------------------------------- %%% @spec does_connection_exist(TargetName,Address,Callback) -> %%% {ok,Handle} | false %%% TargetName = ct:target_name() %%% Address = address %%% Callback = atom() %%% Handle = term() %%% %%% @doc Check if a connection already exists. does_connection_exist(TargetName,Address,Callback) -> case get_ref_from_name(TargetName) of {ok,TargetRef} -> case ets:select(?conn_table,[{#conn{handle='$1', targetref=TargetRef, address=Address, callback=Callback}, [], ['$1']}]) of [Handle] -> {ok,Handle}; [] -> false end; _ -> false end. %%%----------------------------------------------------------------- %%% @spec get_connections(TargetName,Callback) -> %%% {ok,Connections} | {error,Reason} %%% TargetName = ct:target_name() %%% Callback = atom() %%% Connections = [Connection] %%% Connection = {Handle,Address} %%% Handle = term() %%% Address = term() %%% %%% @doc Return all connections for theCallback
on the
%%% given target (TargetName
).
get_connections(TargetName,Callback) ->
case get_ref_from_name(TargetName) of
{ok,Ref} ->
{ok,ets:select(?conn_table,[{#conn{handle='$1',
address='$2',
targetref=Ref,
callback=Callback},
[],
[{{'$1','$2'}}]}])};
Error ->
Error
end.
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:get_target_name/1
get_target_name(ConnPid) ->
case ets:select(?conn_table,[{#conn{handle=ConnPid,targetref='$1',_='_'},
[],
['$1']}]) of
[TargetRef] ->
get_name_from_ref(TargetRef);
[] ->
{error,{unknown_connection,ConnPid}}
end.
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:require/1
require(Key) when is_atom(Key) ->
require({Key,[]});
require({Key,SubKeys}) when is_atom(Key) ->
allocate('_UNDEF',Key,to_list(SubKeys));
require(Key) ->
{error,{invalid,Key}}.
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:require/2
require(Name,Key) when is_atom(Key) ->
require(Name,{Key,[]});
require(Name,{Key,SubKeys}) when is_atom(Name), is_atom(Key) ->
call({require,Name,Key,to_list(SubKeys)});
require(Name,Keys) ->
{error,{invalid,{Name,Keys}}}.
to_list(X) when is_list(X) -> X;
to_list(X) -> [X].
do_require(Name,Key,SubKeys) when is_list(SubKeys) ->
case get_key_from_name(Name) of
{error,_} ->
allocate(Name,Key,SubKeys);
{ok,Key} ->
%% already allocated - check that it has all required subkeys
Vals = [Val || {_Ref,Val} <- lookup_name(Name)],
case get_subconfig(SubKeys,Vals) of
{ok,_SubMapped} ->
ok;
Error ->
Error
end;
{ok,OtherKey} ->
{error,{name_in_use,Name,OtherKey}}
end.
allocate(Name,Key,SubKeys) ->
case ets:match_object(?attr_table,#ct_conf{key=Key,name='_UNDEF',_='_'}) of
[] ->
{error,{not_available,Key}};
Available ->
case allocate_subconfig(Name,SubKeys,Available,false) of
ok ->
ok;
Error ->
Error
end
end.
allocate_subconfig(Name,SubKeys,[C=#ct_conf{value=Value}|Rest],Found) ->
case do_get_config(SubKeys,Value,[]) of
{ok,_SubMapped} ->
ets:insert(?attr_table,C#ct_conf{name=Name}),
allocate_subconfig(Name,SubKeys,Rest,true);
_Error ->
allocate_subconfig(Name,SubKeys,Rest,Found)
end;
allocate_subconfig(_Name,_SubKeys,[],true) ->
ok;
allocate_subconfig(_Name,SubKeys,[],false) ->
{error,{not_available,SubKeys}}.
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:get_config/1
get_config(KeyOrName) ->
get_config(KeyOrName,undefined,[]).
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:get_config/2
get_config(KeyOrName,Default) ->
get_config(KeyOrName,Default,[]).
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:get_config/3
get_config(KeyOrName,Default,Opts) when is_atom(KeyOrName) ->
case lookup_config(KeyOrName) of
[] ->
Default;
[{_Ref,Val}|_] = Vals ->
case {lists:member(all,Opts),lists:member(element,Opts)} of
{true,true} ->
[{KeyOrName,V} || {_R,V} <- lists:sort(Vals)];
{true,false} ->
[V || {_R,V} <- lists:sort(Vals)];
{false,true} ->
{KeyOrName,Val};
{false,false} ->
Val
end
end;
get_config({KeyOrName,SubKey},Default,Opts) ->
case lookup_config(KeyOrName) of
[] ->
Default;
Vals ->
Vals1 = case [Val || {_Ref,Val} <- lists:sort(Vals)] of
Result=[L|_] when is_list(L) ->
case L of
[{_,_}|_] ->
Result;
_ ->
[]
end;
_ ->
[]
end,
case get_subconfig([SubKey],Vals1,[],Opts) of
{ok,[{_,SubVal}|_]=SubVals} ->
case {lists:member(all,Opts),lists:member(element,Opts)} of
{true,true} ->
[{{KeyOrName,SubKey},Val} || {_,Val} <- SubVals];
{true,false} ->
[Val || {_SubKey,Val} <- SubVals];
{false,true} ->
{{KeyOrName,SubKey},SubVal};
{false,false} ->
SubVal
end;
_ ->
Default
end
end.
get_subconfig(SubKeys,Values) ->
get_subconfig(SubKeys,Values,[],[]).
get_subconfig(SubKeys,[Value|Rest],Mapped,Opts) ->
case do_get_config(SubKeys,Value,[]) of
{ok,SubMapped} ->
case lists:member(all,Opts) of
true ->
get_subconfig(SubKeys,Rest,Mapped++SubMapped,Opts);
false ->
{ok,SubMapped}
end;
_Error ->
get_subconfig(SubKeys,Rest,Mapped,Opts)
end;
get_subconfig(SubKeys,[],[],_) ->
{error,{not_available,SubKeys}};
get_subconfig(_SubKeys,[],Mapped,_) ->
{ok,Mapped}.
do_get_config([Key|Required],Available,Mapped) ->
case lists:keysearch(Key,1,Available) of
{value,{Key,Value}} ->
NewAvailable = lists:keydelete(Key,1,Available),
NewMapped = [{Key,Value}|Mapped],
do_get_config(Required,NewAvailable,NewMapped);
false ->
{error,{not_available,Key}}
end;
do_get_config([],_Available,Mapped) ->
{ok,lists:reverse(Mapped)}.
get_all_config() ->
ets:select(?attr_table,[{#ct_conf{name='$1',key='$2',value='$3',
default='$4',_='_'},
[],
[{{'$1','$2','$3','$4'}}]}]).
lookup_config(KeyOrName) ->
case lookup_name(KeyOrName) of
[] ->
lookup_key(KeyOrName);
Values ->
Values
end.
lookup_name(Name) ->
ets:select(?attr_table,[{#ct_conf{ref='$1',value='$2',name=Name,_='_'},
[],
[{{'$1','$2'}}]}]).
lookup_key(Key) ->
ets:select(?attr_table,[{#ct_conf{key=Key,ref='$1',value='$2',name='_UNDEF',_='_'},
[],
[{{'$1','$2'}}]}]).
set_config(Config) ->
set_config('_UNDEF',Config,false).
set_config(Config,Default) ->
set_config('_UNDEF',Config,Default).
set_config(Name,Config,Default) ->
[ets:insert(?attr_table,
#ct_conf{key=Key,value=Val,ref=ct_make_ref(),
name=Name,default=Default}) ||
{Key,Val} <- Config].
delete_config(Default) ->
ets:match_delete(?attr_table,#ct_conf{default=Default,_='_'}),
ok.
%%%-----------------------------------------------------------------
%%% @spec release_allocated() -> ok
%%%
%%% @doc Release all allocated resources, but don't take down any
%%% connections.
release_allocated() ->
Allocated = ets:select(?attr_table,[{#ct_conf{name='$1',_='_'},
[{'=/=','$1','_UNDEF'}],
['$_']}]),
release_allocated(Allocated).
release_allocated([H|T]) ->
ets:delete_object(?attr_table,H),
ets:insert(?attr_table,H#ct_conf{name='_UNDEF'}),
release_allocated(T);
release_allocated([]) ->
ok.
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
update_conf(Name, NewConfig) ->
Old = ets:select(?attr_table,[{#ct_conf{name=Name,_='_'},[],['$_']}]),
lists:foreach(fun(OldElem) ->
NewElem = OldElem#ct_conf{value=NewConfig},
ets:delete_object(?attr_table, OldElem),
ets:insert(?attr_table, NewElem)
end, Old),
ok.
%%%-----------------------------------------------------------------
%%% @spec close_connections() -> ok
%%%
%%% @doc Close all open connections.
close_connections() ->
close_connections(ets:tab2list(?conn_table)),
ok.
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
override_silence_all_connections() ->
Protocols = [telnet,ftp,rpc,snmp],
override_silence_connections(Protocols),
Protocols.
override_silence_connections(Conns) when is_list(Conns) ->
Conns1 = lists:map(fun({C,B}) -> {C,B};
(C) -> {C,true}
end, Conns),
set_testdata({override_silent_connections,Conns1}).
get_overridden_silenced_connections() ->
case get_testdata(override_silent_connections) of
{error,_} ->
undefined;
Conns -> % list() or undefined
Conns
end.
delete_overridden_silenced_connections() ->
delete_testdata(override_silent_connections).
silence_all_connections() ->
Protocols = [telnet,ftp,rpc,snmp],
silence_connections(Protocols),
Protocols.
silence_connections(Conn) when is_tuple(Conn) ->
silence_connections([Conn]);
silence_connections(Conn) when is_atom(Conn) ->
silence_connections([{Conn,true}]);
silence_connections(Conns) when is_list(Conns) ->
Conns1 = lists:map(fun({C,B}) -> {C,B};
(C) -> {C,true}
end, Conns),
set_testdata({silent_connections,Conns1}).
is_silenced(Conn) ->
case get_testdata(silent_connections) of
Conns when is_list(Conns) ->
case lists:keysearch(Conn,1,Conns) of
{value,{Conn,true}} ->
true;
_ ->
false
end;
_ ->
false
end.
reset_silent_connections() ->
delete_testdata(silent_connections).
%%%-----------------------------------------------------------------
%%% @spec stop(How) -> ok
%%%
%%% @doc Stop the ct_util_server and close all existing connections
%%% (tool-internal use only).
%%%
%%% @see ct
stop(How) ->
case whereis(ct_util_server) of
undefined -> ok;
_ -> call({stop,How})
end.
%%%-----------------------------------------------------------------
%%% @spec update_last_run_index() -> ok
%%%
%%% @doc Update ct_run.<timestamp>/index.html
%%% (tool-internal use only).
update_last_run_index() ->
call(update_last_run_index).
%%%-----------------------------------------------------------------
%%% @spec get_mode() -> Mode
%%% Mode = normal | interactive
%%%
%%% @doc Return the current mode of the ct_util_server
%%% (tool-internal use only).
get_mode() ->
call(get_mode).
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:listenv/1
listenv(Telnet) ->
case ct_telnet:send(Telnet,"listenv") of
ok ->
{ok,Data,_} = ct_telnet:expect(Telnet,
["(^.+)=(.*$)"],
[{timeout,seconds(3)},
repeat]),
{ok,[{Name,Val} || [_,Name,Val] <- Data]};
{error,Reason} ->
{error,{could_not_send_command,Telnet,"listenv",Reason}}
end.
%%%-----------------------------------------------------------------
%%% @hidden
%%% @equiv ct:parse_table/1
parse_table(Data) ->
[Heading|Lines]=
[remove_space(string:tokens(L, "|"),[]) || L <- Data, hd(L)==$|],
{Heading,Lines}.
remove_space([Str|Rest],Acc) ->
remove_space(Rest,[string:strip(string:strip(Str),both,$')|Acc]);
remove_space([],Acc) ->
list_to_tuple(lists:reverse(Acc)).
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
is_test_dir(Dir) ->
lists:last(string:tokens(filename:basename(Dir), "_")) == "test".
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
get_testdir(Dir, all) ->
Abs = abs_name(Dir),
case is_test_dir(Abs) of
true ->
Abs;
false ->
AbsTest = filename:join(Abs, "test"),
case filelib:is_dir(AbsTest) of
true -> AbsTest;
false -> Abs
end
end;
get_testdir(Dir, [Suite | _]) when is_atom(Suite) ->
get_testdir(Dir, atom_to_list(Suite));
get_testdir(Dir, [Suite | _]) when is_list(Suite) ->
get_testdir(Dir, Suite);
get_testdir(Dir, Suite) when is_atom(Suite) ->
get_testdir(Dir, atom_to_list(Suite));
get_testdir(Dir, Suite) when is_list(Suite) ->
Abs = abs_name(Dir),
case is_test_dir(Abs) of
true ->
Abs;
false ->
AbsTest = filename:join(Abs, "test"),
Mod = case filename:extension(Suite) of
".erl" -> Suite;
_ -> Suite ++ ".erl"
end,
case filelib:is_file(filename:join(AbsTest, Mod)) of
true -> AbsTest;
false -> Abs
end
end;
get_testdir(Dir, _) ->
get_testdir(Dir, all).
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
encrypt_config_file(SrcFileName, EncryptFileName) ->
case get_crypt_key_from_file() of
{error,_} = E ->
E;
Key ->
encrypt_config_file(SrcFileName, EncryptFileName, {key,Key})
end.
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
encrypt_config_file(SrcFileName, EncryptFileName, {file,KeyFile}) ->
case get_crypt_key_from_file(KeyFile) of
{error,_} = E ->
E;
Key ->
encrypt_config_file(SrcFileName, EncryptFileName, {key,Key})
end;
encrypt_config_file(SrcFileName, EncryptFileName, {key,Key}) ->
crypto:start(),
{K1,K2,K3,IVec} = make_crypto_key(Key),
case file:read_file(SrcFileName) of
{ok,Bin0} ->
Bin1 = term_to_binary({SrcFileName,Bin0}),
Bin2 = case byte_size(Bin1) rem 8 of
0 -> Bin1;
N -> list_to_binary([Bin1,random_bytes(8-N)])
end,
EncBin = crypto:des3_cbc_encrypt(K1, K2, K3, IVec, Bin2),
case file:write_file(EncryptFileName, EncBin) of
ok ->
io:format("~s --(encrypt)--> ~s~n",
[SrcFileName,EncryptFileName]),
ok;
{error,Reason} ->
{error,{Reason,EncryptFileName}}
end;
{error,Reason} ->
{error,{Reason,SrcFileName}}
end.
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
decrypt_config_file(EncryptFileName, TargetFileName) ->
case get_crypt_key_from_file() of
{error,_} = E ->
E;
Key ->
decrypt_config_file(EncryptFileName, TargetFileName, {key,Key})
end.
%%%-----------------------------------------------------------------
%%% @spec
%%%
%%% @doc
decrypt_config_file(EncryptFileName, TargetFileName, {file,KeyFile}) ->
case get_crypt_key_from_file(KeyFile) of
{error,_} = E ->
E;
Key ->
decrypt_config_file(EncryptFileName, TargetFileName, {key,Key})
end;
decrypt_config_file(EncryptFileName, TargetFileName, {key,Key}) ->
crypto:start(),
{K1,K2,K3,IVec} = make_crypto_key(Key),
case file:read_file(EncryptFileName) of
{ok,Bin} ->
DecBin = crypto:des3_cbc_decrypt(K1, K2, K3, IVec, Bin),
case catch binary_to_term(DecBin) of
{'EXIT',_} ->
{error,bad_file};
{_SrcFile,SrcBin} ->
case TargetFileName of
undefined ->
{ok,SrcBin};
_ ->
case file:write_file(TargetFileName, SrcBin) of
ok ->
io:format("~s --(decrypt)--> ~s~n",
[EncryptFileName,TargetFileName]),
ok;
{error,Reason} ->
{error,{Reason,TargetFileName}}
end
end
end;
{error,Reason} ->
{error,{Reason,EncryptFileName}}
end.
get_crypt_key_from_file(File) ->
case file:read_file(File) of
{ok,Bin} ->
case catch string:tokens(binary_to_list(Bin), [$\n,$\r]) of
[Key] ->
Key;
_ ->
{error,{bad_crypt_file,File}}
end;
{error,Reason} ->
{error,{Reason,File}}
end.
get_crypt_key_from_file() ->
CwdFile = filename:join(".",?cryptfile),
{Result,FullName} =
case file:read_file(CwdFile) of
{ok,Bin} ->
{Bin,CwdFile};
_ ->
case init:get_argument(home) of
{ok,[[Home]]} ->
HomeFile = filename:join(Home,?cryptfile),
case file:read_file(HomeFile) of
{ok,Bin} ->
{Bin,HomeFile};
_ ->
{{error,no_crypt_file},noent}
end;
_ ->
{{error,no_crypt_file},noent}
end
end,
case FullName of
noent ->
Result;
_ ->
case catch string:tokens(binary_to_list(Result), [$\n,$\r]) of
[Key] ->
io:format("~nCrypt key file: ~s~n", [FullName]),
Key;
_ ->
{error,{bad_crypt_file,FullName}}
end
end.
make_crypto_key(String) ->
<