%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2004-2016. 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.
%%
%% %CopyrightEnd%
%%
%%
-module(ssh_eqc_client_server).
-compile(export_all).
-include_lib("common_test/include/ct.hrl").
-ifdef(PROPER).
%% Proper is not supported.
-else.
-ifdef(TRIQ).
%% Proper is not supported.
-else.
%% Limit the testing time on CI server... this needs to be improved in % from total budget.
-define(TESTINGTIME(Prop), eqc:testing_time(30,Prop)).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eqc/include/eqc_statem.hrl").
-eqc_group_commands(true).
-define(SSH_DIR,"ssh_eqc_client_server_dirs").
-define(sec, *1000).
-define(min, *60?sec).
-record(srvr,{ref,
address,
port
}).
-record(conn,{ref,
srvr_ref
}).
-record(chan, {ref,
conn_ref,
subsystem,
client_pid
}).
-record(state,{
initialized = false,
servers = [], % [#srvr{}]
clients = [],
connections = [], % [#conn{}]
channels = [], % [#chan{}]
data_dir
}).
%%%===============================================================
%%%
%%% Specification of addresses, subsystems and such.
%%%
-define(MAX_NUM_SERVERS, 3).
-define(MAX_NUM_CLIENTS, 3).
-define(SUBSYSTEMS, ["echo1", "echo2", "echo3", "echo4"]).
-define(SERVER_ADDRESS, { {127,1,0,choose(1,254)}, % IP
choose(1024,65535) % Port
}).
-define(SERVER_EXTRA_OPTIONS, [{parallel_login,bool()}] ).
%%%================================================================
%%%
%%% The properties - one sequantial and one parallel with the same model
%%%
%%% Run as
%%%
%%% $ (cd ..; make)
%%% $ erl -pz ..
%%%
%%% eqc:quickcheck( ssh_eqc_client_server:prop_seq() ).
%%% eqc:quickcheck( ssh_eqc_client_server:prop_parallel() ).
%%% eqc:quickcheck( ssh_eqc_client_server:prop_parallel_multi() ).
%%%
%% To be called as eqc:quickcheck( ssh_eqc_client_server:prop_seq() ).
prop_seq() ->
?TESTINGTIME(do_prop_seq(?SSH_DIR)).
%% To be called from a common_test test suite
prop_seq(CT_Config) ->
do_prop_seq(full_path(?SSH_DIR, CT_Config)).
do_prop_seq(DataDir) ->
setup_rsa(DataDir),
?FORALL(Cmds,commands(?MODULE),
begin
{H,Sf,Result} = run_commands(?MODULE,Cmds,[{data_dir,DataDir}]),
present_result(?MODULE, Cmds, {H,Sf,Result}, Result==ok)
end).
full_path(SSHdir, CT_Config) ->
filename:join(proplists:get_value(property_dir, CT_Config),
SSHdir).
%%%----
prop_parallel() ->
?TESTINGTIME(do_prop_parallel(?SSH_DIR)).
%% To be called from a common_test test suite
prop_parallel(CT_Config) ->
do_prop_parallel(full_path(?SSH_DIR, CT_Config)).
do_prop_parallel(DataDir) ->
setup_rsa(DataDir),
?FORALL(Cmds,parallel_commands(?MODULE),
begin
{H,Sf,Result} = run_parallel_commands(?MODULE,Cmds,[{data_dir,DataDir}]),
present_result(?MODULE, Cmds, {H,Sf,Result}, Result==ok)
end).
%%%----
prop_parallel_multi() ->
?TESTINGTIME(do_prop_parallel_multi(?SSH_DIR)).
%% To be called from a common_test test suite
prop_parallel_multi(CT_Config) ->
do_prop_parallel_multi(full_path(?SSH_DIR, CT_Config)).
do_prop_parallel_multi(DataDir) ->
setup_rsa(DataDir),
?FORALL(Repetitions,?SHRINK(1,[10]),
?FORALL(Cmds,parallel_commands(?MODULE),
?ALWAYS(Repetitions,
begin
{H,Sf,Result} = run_parallel_commands(?MODULE,Cmds,[{data_dir,DataDir}]),
present_result(?MODULE, Cmds, {H,Sf,Result}, Result==ok)
end))).
%%%================================================================
%%% State machine spec
%%% called when using commands/1
initial_state() ->
#state{}.
%%% called when using commands/2
initial_state(DataDir) ->
application:stop(ssh),
ssh:start().
%%%----------------
weight(S, ssh_send) -> 5*length([C || C<-S#state.channels, has_subsyst(C)]);
weight(S, ssh_start_subsyst) -> 3*length([C || C<-S#state.channels, no_subsyst(C)]);
weight(S, ssh_close_channel) -> 2*length([C || C<-S#state.channels, has_subsyst(C)]);
weight(S, ssh_open_channel) -> length(S#state.connections);
weight(_S, _) -> 1.
%%%----------------
%%% Initialize
initial_state_pre(S) -> not S#state.initialized.
initial_state_args(_) -> [{var,data_dir}].
initial_state_next(S, _, _) -> S#state{initialized=true}.
%%%----------------
%%% Start a new daemon
%%% Precondition: not more than ?MAX_NUM_SERVERS started
%%% This is a bit funny because we need to pick an IP address and Port to
%%% run the server on, but there is no way to atomically select a free Port!
%%%
%%% Therefore we just grab one IP-Port pair randomly and try to start the ssh server
%%% on that pair. If it fails, we just forget about it and goes on. Yes, it
%%% is a waste of cpu cycles, but at least it works!
ssh_server_pre(S) -> S#state.initialized andalso
length(S#state.servers) < ?MAX_NUM_SERVERS.
ssh_server_args(_) -> [?SERVER_ADDRESS, {var,data_dir}, ?SERVER_EXTRA_OPTIONS].
ssh_server({IP,Port}, DataDir, ExtraOptions) ->
ok(ssh:daemon(IP, Port,
[
{system_dir, system_dir(DataDir)},
{user_dir, user_dir(DataDir)},
{subsystems, [{SS, {ssh_eqc_subsys, [SS]}} || SS <- ?SUBSYSTEMS]}
| ExtraOptions
])).
ssh_server_post(_S, _Args, {error,eaddrinuse}) -> true;
ssh_server_post(_S, _Args, Result) -> is_ok(Result).
ssh_server_next(S, {error,eaddrinuse}, _) -> S;
ssh_server_next(S, Result, [{IP,Port},_,_]) ->
S#state{servers=[#srvr{ref = Result,
address = IP,
port = Port}
| S#state.servers]}.
%%%----------------
%%% Start a new client
%%% Precondition: not more than ?MAX_NUM_CLIENTS started
ssh_client_pre(S) -> S#state.initialized andalso
length(S#state.clients) < ?MAX_NUM_CLIENTS.
ssh_client_args(_S) -> [].
ssh_client() -> spawn(fun client_init/0).
ssh_client_next(S, Pid, _) -> S#state{clients=[Pid|S#state.clients]}.
client_init() -> client_loop().
client_loop() ->
receive
{please_do,Fun,Ref,Pid} ->
Pid ! {my_pleasure, catch Fun(), Ref},
client_loop()
end.
do(Pid, Fun) -> do(Pid, Fun, 30?sec).
do(Pid, Fun, Timeout) when is_function(Fun,0) ->
Pid ! {please_do,Fun,Ref=make_ref(),self()},
receive
{my_pleasure, Result, Ref} -> Result
after
Timeout -> {error,do_timeout}
end.
%%%----------------
%%% Start a new connection
%%% Precondition: deamon exists
ssh_open_connection_pre(S) -> S#state.servers /= [].
ssh_open_connection_args(S) -> [oneof(S#state.servers), {var,data_dir}].
ssh_open_connection(#srvr{address=Ip, port=Port}, DataDir) ->
ok(ssh:connect(ensure_string(Ip), Port,
[
{silently_accept_hosts, true},
{user_dir, user_dir(DataDir)},
{user_interaction, false},
{connect_timeout, 2000}
], 2000)).
ssh_open_connection_post(_S, _Args, Result) -> is_ok(Result).
ssh_open_connection_next(S, ConnRef, [#srvr{ref=SrvrRef},_]) ->
S#state{connections=[#conn{ref=ConnRef, srvr_ref=SrvrRef}|S#state.connections]}.
%%%----------------
%%% Stop a new connection
%%% Precondition: connection exists
ssh_close_connection_pre(S) -> S#state.connections /= [].
ssh_close_connection_args(S) -> [oneof(S#state.connections)].
ssh_close_connection(#conn{ref=ConnectionRef}) -> ssh:close(ConnectionRef).
ssh_close_connection_next(S, _, [Conn=#conn{ref=ConnRef}]) ->
S#state{connections = S#state.connections--[Conn],
channels = [C || C <- S#state.channels,
C#chan.conn_ref /= ConnRef]
}.
%%%----------------
%%% Start a new channel without a sub system
%%% Precondition: connection exists
ssh_open_channel_pre(S) -> S#state.connections /= [].
ssh_open_channel_args(S) -> [oneof(S#state.connections)].
%%% For re-arrangement in parallel tests.
ssh_open_channel_pre(S,[C]) -> lists:member(C,S#state.connections).
ssh_open_channel(#conn{ref=ConnectionRef}) ->
ok(ssh_connection:session_channel(ConnectionRef, 20?sec)).
ssh_open_channel_post(_S, _Args, Result) -> is_ok(Result).
ssh_open_channel_next(S, ChannelRef, [#conn{ref=ConnRef}]) ->
S#state{channels=[#chan{ref=ChannelRef,
conn_ref=ConnRef}
| S#state.channels]}.
%%%----------------
%%% Stop a channel
%%% Precondition: a channel exists, with or without a subsystem
ssh_close_channel_pre(S) -> S#state.channels /= [].
ssh_close_channel_args(S) -> [oneof(S#state.channels)].
ssh_close_channel(#chan{ref=ChannelRef, conn_ref=ConnectionRef}) ->
ssh_connection:close(ConnectionRef, ChannelRef).
ssh_close_channel_next(S, _, [C]) ->
S#state{channels = [Ci || Ci <- S#state.channels,
sig(C) /= sig(Ci)]}.
sig(C) -> {C#chan.ref, C#chan.conn_ref}.
%%%----------------
%%% Start a sub system on a channel
%%% Precondition: A channel without subsystem exists
ssh_start_subsyst_pre(S) -> lists:any(fun no_subsyst/1, S#state.channels) andalso
S#state.clients /= [].
ssh_start_subsyst_args(S) -> [oneof(lists:filter(fun no_subsyst/1, S#state.channels)),
oneof(?SUBSYSTEMS),
oneof(S#state.clients)
].
%% For re-arrangement in parallel tests.
ssh_start_subsyst_pre(S, [C|_]) -> lists:member(C,S#state.channels)
andalso no_subsyst(C).
ssh_start_subsyst(#chan{ref=ChannelRef, conn_ref=ConnectionRef}, SubSystem, Pid) ->
do(Pid, fun()->ssh_connection:subsystem(ConnectionRef, ChannelRef, SubSystem, 120?sec) end).
ssh_start_subsyst_post(_S, _Args, Result) -> Result==success.
ssh_start_subsyst_next(S, _Result, [C,SS,Pid|_]) ->
S#state{channels = [C#chan{subsystem=SS,
client_pid=Pid}|(S#state.channels--[C])] }.
%%%----------------
%%% Send a message on a channel
%%% Precondition: a channel exists with a subsystem connected
ssh_send_pre(S) -> lists:any(fun has_subsyst/1, S#state.channels).
ssh_send_args(S) -> [oneof(lists:filter(fun has_subsyst/1, S#state.channels)),
choose(0,1),
message()].
%% For re-arrangement in parallel tests.
ssh_send_pre(S, [C|_]) -> lists:member(C, S#state.channels).
ssh_send(C=#chan{conn_ref=ConnectionRef, ref=ChannelRef, client_pid=Pid}, Type, Msg) ->
do(Pid,
fun() ->
case ssh_connection:send(ConnectionRef, ChannelRef, Type, modify_msg(C,Msg), 10?sec) of
ok ->
receive
{ssh_cm,ConnectionRef,{data,ChannelRef,Type,Answer}} -> Answer
after 15?sec ->
%% receive
%% Other -> {error,{unexpected,Other}}
%% after 0 ->
{error,receive_timeout}
%% end
end;
Other ->
Other
end
end).
ssh_send_blocking(_S, _Args) ->
true.
ssh_send_post(_S, [C,_,Msg], Response) when is_binary(Response) ->
Expected = ssh_eqc_subsys:response(modify_msg(C,Msg), C#chan.subsystem),
case Response of
Expected -> true;
_ -> {send_failed, size(Response), size(Expected)}
end;
ssh_send_post(_S, _Args, Response) ->
{error,Response}.
modify_msg(_, <<>>) -> <<>>;
modify_msg(#chan{subsystem=SS}, Msg) -> <<(list_to_binary(SS))/binary,Msg/binary>>.
%%%================================================================
%%% Misc functions
message() ->
resize(500, binary()).
%% binary().
%% oneof([binary(),
%% ?LET(Size, choose(0,10000), binary(Size))
%% ]).
has_subsyst(C) -> C#chan.subsystem /= undefined.
no_subsyst(C) -> not has_subsyst(C).
ok({ok,X}) -> X;
ok({error,Err}) -> {error,Err}.
is_ok({error,_}) -> false;
is_ok(_) -> true.
ensure_string({A,B,C,D}) -> lists:flatten(io_lib:format("~w.~w.~w.~w",[A,B,C,D]));
ensure_string(X) -> X.
%%%----------------------------------------------------------------
present_result(_Module, Cmds, _Triple, true) ->
aggregate(with_title("Distribution sequential/parallel"), sequential_parallel(Cmds),
aggregate(with_title("Function calls"), cmnd_names(Cmds),
aggregate(with_title("Message sizes"), empty_msgs(Cmds),
aggregate(print_frequencies(), message_sizes(Cmds),
aggregate(title("Length of command sequences",print_frequencies()), num_calls(Cmds),
true)))));
present_result(Module, Cmds, Triple, false) ->
pretty_commands(Module, Cmds, Triple, [{show_states,true}], false).
cmnd_names(Cs) -> traverse_commands(fun cmnd_name/1, Cs).
cmnd_name(L) -> [F || {set,_Var,{call,_Mod,F,_As}} <- L].
empty_msgs(Cs) -> traverse_commands(fun empty_msg/1, Cs).
empty_msg(L) -> [empty || {set,_,{call,_,ssh_send,[_,_,Msg]}} <- L,
size(Msg)==0].
message_sizes(Cs) -> traverse_commands(fun message_size/1, Cs).
message_size(L) -> [size(Msg) || {set,_,{call,_,ssh_send,[_,_,Msg]}} <- L].
num_calls(Cs) -> traverse_commands(fun num_call/1, Cs).
num_call(L) -> [length(L)].
sequential_parallel(Cs) ->
traverse_commands(fun(L) -> dup_module(L, sequential) end,
fun(L) -> [dup_module(L1, mkmod("parallel",num(L1,L))) || L1<-L] end,
Cs).
dup_module(L, ModName) -> lists:duplicate(length(L), ModName).
mkmod(PfxStr,N) -> list_to_atom(PfxStr++"_"++integer_to_list(N)).
%% Meta functions for the aggregate functions
traverse_commands(Fun, L) when is_list(L) -> Fun(L);
traverse_commands(Fun, {Seq, ParLs}) -> Fun(lists:append([Seq|ParLs])).
traverse_commands(Fseq, _Fpar, L) when is_list(L) -> Fseq(L);
traverse_commands(Fseq, Fpar, {Seq, ParLs}) -> lists:append([Fseq(Seq)|Fpar(ParLs)]).
%%%----------------
%% PrintMethod([{term(), int()}]) -> any().
print_frequencies() -> print_frequencies(10).
print_frequencies(Ngroups) -> fun([]) -> io:format('Empty list!~n',[]);
(L ) ->
try
M = lists:last(L),
Max = if is_integer(M) -> M;
is_tuple(M) -> element(1,L)
end,
print_frequencies(L,Ngroups,0,Max)
catch
C:E:S ->
ct:pal("~p:~p ~p:~p~n~p~n~p",[?MODULE,?LINE,C,E,S,L])
end
end.
print_frequencies(Ngroups, MaxValue) -> fun(L) -> print_frequencies(L,Ngroups,0,MaxValue) end.
print_frequencies(L, N, Min, Max) when N>Max -> print_frequencies(L++[{N,0}], N, Min, N);
print_frequencies(L, N, Min, Max0) ->
try
Interval = round((Max0-Min)/N),
Max = Max0 + (Max0 rem Interval),
IntervalUpperLimits =
lists:reverse(
[Max | tl(lists:reverse(lists:seq(Min,Max,Interval)))]
),
{Acc0,_} = lists:mapfoldl(fun(Upper,Lower) ->
{{{Lower,Upper},0}, Upper+1}
end, hd(IntervalUpperLimits), tl(IntervalUpperLimits)),
Fs0 = get_frequencies(L, Acc0),
SumVal = lists:sum([V||{_,V}<-Fs0]),
Fs = with_percentage(Fs0, SumVal),
Mean = mean(L),
Median = median(L),
Npos_value = num_digits(SumVal),
Npos_range = num_digits(Max),
io:format("Range~*s: ~s~n",[2*Npos_range-2,"", "Number in range"]),
io:format("~*c:~*c~n",[2*Npos_range+3,$-, max(16,Npos_value+10),$- ]),
[begin
io:format("~*w - ~*w: ~*w ~5.1f%",[Npos_range,Rlow,
Npos_range,Rhigh,
Npos_value,Val,
Percent]),
[io:format(" <-- mean=~.1f",[Mean]) || in_interval(Mean, Interval)],
[io:format(" <-- median=" ++
if
is_float(Median) -> "~.1f";
true -> "~p"
end, [Median]) || in_interval(Median, Interval)],
io:nl()
end
|| {Interval={Rlow,Rhigh},Val,Percent} <- Fs],
io:format('~*c ~*c~n',[2*Npos_range,32,Npos_value+2,$-]),
io:format('~*c ~*w~n',[2*Npos_range,32,Npos_value,SumVal])
catch
C:E ->
io:format('*** Faild printing (~p:~p) for~n~p~n',[C,E,L])
end.
get_frequencies([{I,Num}|T], [{{Lower,Upper},Cnt}|Acc]) when Lower=<I,I=<Upper ->
get_frequencies(T, [{{Lower,Upper},Cnt+Num}|Acc]);
get_frequencies(L=[{I,_Num}|_], [Ah={{_Lower,Upper},_Cnt}|Acc]) when I>Upper ->
[Ah | get_frequencies(L,Acc)];
get_frequencies([I|T], Acc) when is_integer(I) ->
get_frequencies([{I,1}|T], Acc);
get_frequencies([], Acc) ->
Acc.
with_percentage(Fs, Sum) ->
[{Rng,Val,100*Val/Sum} || {Rng,Val} <- Fs].
title(Str, Fun) ->
fun(L) ->
io:format('~s~n',[Str]),
Fun(L)
end.
num_digits(I) -> 1+trunc(math:log(I)/math:log(10)).
num(Elem, List) -> length(lists:takewhile(fun(E) -> E /= Elem end, List)) + 1.
%%%---- Just for naming an operation for readability
is_odd(I) -> (I rem 2) == 1.
in_interval(Value, {Rlow,Rhigh}) ->
try
Rlow=<round(Value) andalso round(Value)=<Rhigh
catch
_:_ -> false
end.
%%%================================================================
%%% Statistical functions
%%%---- Mean value
mean(L = [X|_]) when is_number(X) ->
lists:sum(L) / length(L);
mean(L = [{_Value,_Weight}|_]) ->
SumOfWeights = lists:sum([W||{_,W}<-L]),
WeightedSum = lists:sum([W*V||{V,W}<-L]),
WeightedSum / SumOfWeights;
mean(_) ->
undefined.
%%%---- Median
median(L = [X|_]) when is_number(X) ->
case is_odd(length(L)) of
true ->
hd(lists:nthtail(length(L) div 2, L));
false ->
%% 1) L has at least on element (the when test).
%% 2) Length is even.
%% => Length >= 2
[M1,M2|_] = lists:nthtail((length(L) div 2)-1, L),
(M1+M2) / 2
end;
%% integer Weights...
median(L = [{_Value,_Weight}|_]) ->
median( lists:append([lists:duplicate(W,V) || {V,W} <- L]) );
median(_) ->
undefined.
%%%================================================================
%%% The rest is taken and modified from ssh_test_lib.erl
setup_rsa(Dir) ->
erase_dir(system_dir(Dir)),
erase_dir(user_dir(Dir)),
file:make_dir(system_dir(Dir)),
file:make_dir(user_dir(Dir)),
file:copy(data_dir(Dir,"id_rsa"), user_dir(Dir,"id_rsa")),
file:copy(data_dir(Dir,"ssh_host_rsa_key"), system_dir(Dir,"ssh_host_rsa_key")),
file:copy(data_dir(Dir,"ssh_host_rsa_key"), system_dir(Dir,"ssh_host_rsa_key.pub")),
ssh_test_lib:setup_rsa_known_host(data_dir(Dir), user_dir(Dir)),
ssh_test_lib:setup_rsa_auth_keys(data_dir(Dir), user_dir(Dir)).
data_dir(Dir, File) -> filename:join(Dir, File).
system_dir(Dir, File) -> filename:join([Dir, "system", File]).
user_dir(Dir, File) -> filename:join([Dir, "user", File]).
data_dir(Dir) -> Dir.
system_dir(Dir) -> system_dir(Dir,"").
user_dir(Dir) -> user_dir(Dir,"").
erase_dir(Dir) ->
case file:list_dir(Dir) of
{ok,Files} -> lists:foreach(fun(F) -> file:delete(filename:join(Dir,F)) end,
Files);
_ -> ok
end,
file:del_dir(Dir).
-endif.
-endif.