aboutsummaryrefslogtreecommitdiffstats
path: root/lib/ssh/test/property_test/ssh_eqc_client_server.erl
diff options
context:
space:
mode:
authorHans Nilsson <[email protected]>2014-09-02 17:05:38 +0200
committerHans Nilsson <[email protected]>2014-09-02 17:05:38 +0200
commit5e7c707b1a7e98d75aa9edeaaf3f2b15d7f27110 (patch)
tree52c528e63f4490d50f4151db0d2586f2c9d94959 /lib/ssh/test/property_test/ssh_eqc_client_server.erl
parentb3e08774fa418c3adae11d823fc7e9328674b4d2 (diff)
downloadotp-5e7c707b1a7e98d75aa9edeaaf3f2b15d7f27110.tar.gz
otp-5e7c707b1a7e98d75aa9edeaaf3f2b15d7f27110.tar.bz2
otp-5e7c707b1a7e98d75aa9edeaaf3f2b15d7f27110.zip
ssh: Add simple experimental property test suites
Diffstat (limited to 'lib/ssh/test/property_test/ssh_eqc_client_server.erl')
-rw-r--r--lib/ssh/test/property_test/ssh_eqc_client_server.erl602
1 files changed, 602 insertions, 0 deletions
diff --git a/lib/ssh/test/property_test/ssh_eqc_client_server.erl b/lib/ssh/test/property_test/ssh_eqc_client_server.erl
new file mode 100644
index 0000000000..3a84acebb3
--- /dev/null
+++ b/lib/ssh/test/property_test/ssh_eqc_client_server.erl
@@ -0,0 +1,602 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2004-2014. 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%
+%%
+%%
+
+-module(ssh_eqc_client_server).
+
+-compile(export_all).
+
+-include_lib("common_test/include/ct.hrl").
+
+-ifdef(PROPER).
+%% Proper is not supported.
+-else.
+
+-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,1,1}, inet_port({127,1,1,1}) }).
+
+-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() ->
+ 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) ->
+ ?FORALL(Cmds,commands(?MODULE, #state{data_dir=DataDir}),
+ begin
+ {H,Sf,Result} = run_commands(?MODULE,Cmds),
+ 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() ->
+ 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) ->
+ ?FORALL(Cmds,parallel_commands(?MODULE, #state{data_dir=DataDir}),
+ begin
+ {H,Sf,Result} = run_parallel_commands(?MODULE,Cmds),
+ present_result(?MODULE, Cmds, {H,Sf,Result}, Result==ok)
+ end).
+
+%%%----
+prop_parallel_multi() ->
+ 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) ->
+ ?FORALL(Repetitions,?SHRINK(1,[10]),
+ ?FORALL(Cmds,parallel_commands(?MODULE, #state{data_dir=DataDir}),
+ ?ALWAYS(Repetitions,
+ begin
+ {H,Sf,Result} = run_parallel_commands(?MODULE,Cmds),
+ present_result(?MODULE, Cmds, {H,Sf,Result}, Result==ok)
+ end))).
+
+%%%================================================================
+%%% State machine spec
+
+%%% called when using commands/1
+initial_state() ->
+ S = initial_state(#state{}),
+ S#state{initialized=true}.
+
+%%% called when using commands/2
+initial_state(S) ->
+ application:stop(ssh),
+ ssh:start(),
+ setup_rsa(S#state.data_dir).
+
+%%%----------------
+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(S) -> [S].
+
+initial_state_next(S, _, _) -> S#state{initialized=true}.
+
+%%%----------------
+%%% Start a new daemon
+%%% Precondition: not more than ?MAX_NUM_SERVERS started
+
+ssh_server_pre(S) -> S#state.initialized andalso
+ length(S#state.servers) < ?MAX_NUM_SERVERS.
+
+ssh_server_args(S) -> [?SERVER_ADDRESS, S#state.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, Result) -> is_ok(Result).
+
+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), S#state.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}
+ ])).
+
+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 ) -> print_frequencies(L,Ngroups,0,element(1,lists:last(L)))
+ 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, Max) ->
+%%io:format('L=~p~n',[L]),
+ try
+ IntervalUpperLimits =
+ lists:reverse(
+ [Max | tl(lists:reverse(lists:seq(Min,Max,round((Max-Min)/N))))]
+ ),
+ {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])
+ %%,io:format('L=~p~n',[L])
+ 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([], 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
+inet_port(IpAddress)->
+ {ok, Socket} = gen_tcp:listen(0, [{ip,IpAddress},{reuseaddr,true}]),
+ {ok, Port} = inet:port(Socket),
+ gen_tcp:close(Socket),
+ Port.
+
+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.