%%
%% %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(ftp_simple_client_server).
-compile(export_all).
-ifndef(EQC).
-ifndef(PROPER).
-define(EQC,true).
%%-define(PROPER,true).
-endif.
-endif.
-ifdef(EQC).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eqc/include/eqc_statem.hrl").
-define(MOD_eqc, eqc).
-define(MOD_eqc_gen, eqc_gen).
-define(MOD_eqc_statem, eqc_statem).
-else.
-ifdef(PROPER).
-include_lib("proper/include/proper.hrl").
-define(MOD_eqc, proper).
-define(MOD_eqc_gen, proper_gen).
-define(MOD_eqc_statem, proper_statem).
-endif.
-endif.
-record(state, {
initialized = false,
priv_dir,
data_dir,
servers = [], % [ {IP,Port,Userid,Pwd} ]
clients = [], % [ client_ref() ]
store = [] % [ {Name,Contents} ]
}).
-define(fmt(F,A), io:format(F,A)).
%%-define(fmt(F,A), ok).
-define(v(K,L), proplists:get_value(K,L)).
%%%================================================================
%%%
%%% Properties
%%%
%% This function is for normal eqc calls:
prop_ftp() ->
{ok,PWD} = file:get_cwd(),
prop_ftp(filename:join([PWD,?MODULE_STRING++"_data"]),
filename:join([PWD,?MODULE_STRING,"_files"])).
%% This function is for calls from common_test test cases:
prop_ftp(Config) ->
prop_ftp(filename:join([?v(property_dir,Config), ?MODULE_STRING++"_data"]),
?v(priv_dir,Config) ).
prop_ftp(DataDir, PrivDir) ->
S0 = #state{data_dir = DataDir,
priv_dir = PrivDir},
?FORALL(Cmds, more_commands(10,commands(?MODULE,S0)),
aggregate(command_names(Cmds),
begin {_H,S,Result} = run_commands(?MODULE,Cmds),
% io:format('**** Result=~p~n',[Result]),
% io:format('**** S=~p~n',[S]),
% io:format('**** _H=~p~n',[_H]),
% io:format('**** Cmds=~p~n',[Cmds]),
[cmnd_stop_server(X) || X <- S#state.servers],
[inets:stop(ftpc,X) || {ok,X} <- S#state.clients],
Result==ok
end)
).
%%%================================================================
%%%
%%% State model
%%%
%% @doc Returns the state in which each test case starts. (Unless a different
%% initial state is supplied explicitly to, e.g. commands/2.)
-spec initial_state() ->?MOD_eqc_statem:symbolic_state().
initial_state() ->
?fmt("Initial_state()~n",[]),
#state{}.
%% @doc Command generator, S is the current state
-spec command(S :: ?MOD_eqc_statem:symbolic_state()) -> ?MOD_eqc_gen:gen(eqc_statem:call()).
command(#state{initialized=false,
priv_dir=PrivDir}) ->
{call,?MODULE,cmnd_init,[PrivDir]};
command(#state{servers=[],
priv_dir=PrivDir,
data_dir=DataDir}) ->
{call,?MODULE,cmnd_start_server,[PrivDir,DataDir]};
command(#state{servers=Ss=[_|_],
clients=[]}) ->
{call,?MODULE,cmnd_start_client,[oneof(Ss)]};
command(#state{servers=Ss=[_|_],
clients=Cs=[_|_],
store=Store=[_|_]
}) ->
frequency([
{ 5, {call,?MODULE,cmnd_start_client,[oneof(Ss)]}},
{ 5, {call,?MODULE,cmnd_stop_client,[oneof(Cs)]}},
{10, {call,?MODULE,cmnd_put,[oneof(Cs),file_path(),file_contents()]}},
{20, {call,?MODULE,cmnd_get,[oneof(Cs),oneof(Store)]}},
{10, {call,?MODULE,cmnd_delete,[oneof(Cs),oneof(Store)]}}
]);
command(#state{servers=Ss=[_|_],
clients=Cs=[_|_],
store=[]
}) ->
frequency([
{5, {call,?MODULE,cmnd_start_client,[oneof(Ss)]}},
{5, {call,?MODULE,cmnd_stop_client,[oneof(Cs)]}},
{10, {call,?MODULE,cmnd_put,[oneof(Cs),file_path(),file_contents()]}}
]).
%% @doc Precondition, checked before command is added to the command sequence.
-spec precondition(S :: ?MOD_eqc_statem:symbolic_state(), C :: ?MOD_eqc_statem:call()) -> boolean().
precondition(#state{clients=Cs}, {call, _, cmnd_put, [C,_,_]}) -> lists:member(C,Cs);
precondition(#state{clients=Cs, store=Store},
{call, _, cmnd_get, [C,X]}) -> lists:member(C,Cs) andalso lists:member(X,Store);
precondition(#state{clients=Cs, store=Store},
{call, _, cmnd_delete, [C,X]}) -> lists:member(C,Cs) andalso lists:member(X,Store);
precondition(#state{servers=Ss}, {call, _, cmnd_start_client, _}) -> Ss =/= [];
precondition(#state{clients=Cs}, {call, _, cmnd_stop_client, [C]}) -> lists:member(C,Cs);
precondition(#state{initialized=IsInit}, {call, _, cmnd_init, _}) -> IsInit==false;
precondition(_S, {call, _, _, _}) -> true.
%% @doc Postcondition, checked after command has been evaluated
%% Note: S is the state before next_state(S,_,C)
-spec postcondition(S :: ?MOD_eqc_statem:dynamic_state(), C :: ?MOD_eqc_statem:call(),
Res :: term()) -> boolean().
postcondition(_S, {call, _, cmnd_get, [_,{_Name,Expected}]}, {ok,Value}) ->
Value == Expected;
postcondition(S, {call, _, cmnd_delete, [_,{Name,_Expected}]}, ok) ->
?fmt("file:read_file(..) = ~p~n",[file:read_file(filename:join(S#state.priv_dir,Name))]),
{error,enoent} == file:read_file(filename:join(S#state.priv_dir,Name));
postcondition(S, {call, _, cmnd_put, [_,Name,Value]}, ok) ->
{ok,Bin} = file:read_file(filename:join(S#state.priv_dir,Name)),
Bin == unicode:characters_to_binary(Value);
postcondition(_S, {call, _, cmnd_stop_client, _}, ok) -> true;
postcondition(_S, {call, _, cmnd_start_client, _}, {ok,_}) -> true;
postcondition(_S, {call, _, cmnd_init, _}, ok) -> true;
postcondition(_S, {call, _, cmnd_start_server, _}, {ok,_}) -> true.
%% @doc Next state transformation, S is the current state. Returns next state.
-spec next_state(S :: ?MOD_eqc_statem:symbolic_state(),
V :: ?MOD_eqc_statem:var(),
C :: ?MOD_eqc_statem:call()) -> ?MOD_eqc_statem:symbolic_state().
next_state(S, _V, {call, _, cmnd_put, [_,Name,Val]}) ->
S#state{store = [{Name,Val} | lists:keydelete(Name,1,S#state.store)]};
next_state(S, _V, {call, _, cmnd_delete, [_,{Name,_Val}]}) ->
S#state{store = lists:keydelete(Name,1,S#state.store)};
next_state(S, V, {call, _, cmnd_start_client, _}) ->
S#state{clients = [V | S#state.clients]};
next_state(S, V, {call, _, cmnd_start_server, _}) ->
S#state{servers = [V | S#state.servers]};
next_state(S, _V, {call, _, cmnd_stop_client, [C]}) ->
S#state{clients = S#state.clients -- [C]};
next_state(S, _V, {call, _, cmnd_init, _}) ->
S#state{initialized=true};
next_state(S, _V, {call, _, _, _}) ->
S.
%%%================================================================
%%%
%%% Data model
%%%
file_path() -> non_empty(list(alphanum_char())).
%%file_path() -> non_empty( list(oneof([alphanum_char(), utf8_char()])) ).
%%file_contents() -> list(alphanum_char()).
file_contents() -> list(oneof([alphanum_char(), utf8_char()])).
alphanum_char() -> oneof(lists:seq($a,$z) ++ lists:seq($A,$Z) ++ lists:seq($0,$9)).
utf8_char() -> oneof("åäöÅÄÖ話话カタカナひらがな").
%%%================================================================
%%%
%%% Commands doing something with the System Under Test
%%%
cmnd_init(PrivDir) ->
?fmt('Call cmnd_init(~p)~n',[PrivDir]),
os:cmd("killall vsftpd"),
clear_files(PrivDir),
ok.
cmnd_start_server(PrivDir, DataDir) ->
?fmt('Call cmnd_start_server(~p, ~p)~n',[PrivDir,DataDir]),
Cmnd = ["vsftpd ", filename:join(DataDir,"vsftpd.conf"),
" -oftpd_banner=erlang_otp_testing"
" -oanon_root=",PrivDir
],
?fmt("Cmnd=~s~n",[Cmnd]),
case os:cmd(Cmnd) of
[] ->
{ok,{"localhost",9999,"ftp","[email protected]"}};
Other ->
{error,Other}
end.
cmnd_stop_server({ok,{_Host,Port,_Usr,_Pwd}}) ->
os:cmd("kill `netstat -tpln | grep "++integer_to_list(Port)++" | awk '{print $7}' | awk -F/ '{print $1}'`").
cmnd_start_client({ok,{Host,Port,Usr,Pwd}}) ->
?fmt('Call cmnd_start_client(~p)...',[{Host,Port,Usr,Pwd}]),
case inets:start(ftpc, [{host,Host},{port,Port}]) of
{ok,Client} ->
?fmt("~p...",[{ok,Client}]),
case ftp:user(Client, Usr, Pwd) of
ok ->
?fmt("OK!~n",[]),
{ok,Client};
Other ->
?fmt("Other1=~p~n",[Other]),
inets:stop(ftpc,Client), Other
end;
Other ->
?fmt("Other2=~p~n",[Other]),
Other
end.
cmnd_stop_client({ok,Client}) ->
?fmt('Call cmnd_stop_client(~p)~n',[Client]),
inets:stop(ftpc, Client). %% -> ok | Other
cmnd_delete({ok,Client}, {Name,_ExpectedValue}) ->
?fmt('Call cmnd_delete(~p, ~p)~n',[Client,Name]),
R=ftp:delete(Client, Name),
?fmt("R=~p~n",[R]),
R.
cmnd_put({ok,Client}, Name, Value) ->
?fmt('Call cmnd_put(~p, ~p, ~p)...',[Client, Name, Value]),
R = ftp:send_bin(Client, unicode:characters_to_binary(Value), Name), % ok | {error,Error}
?fmt('~p~n',[R]),
R.
cmnd_get({ok,Client}, {Name,_ExpectedValue}) ->
?fmt('Call cmnd_get(~p, ~p)~n',[Client,Name]),
case ftp:recv_bin(Client, Name) of
{ok,Bin} -> {ok, unicode:characters_to_list(Bin)};
Other -> Other
end.
clear_files(Dir) ->
os:cmd(["rm -fr ",filename:join(Dir,"*")]).