aboutsummaryrefslogtreecommitdiffstats
path: root/lib/common_test/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common_test/src')
-rw-r--r--lib/common_test/src/Makefile6
-rw-r--r--lib/common_test/src/common_test.app.src4
-rw-r--r--lib/common_test/src/ct_cover.erl18
-rw-r--r--lib/common_test/src/ct_logs.erl67
-rw-r--r--lib/common_test/src/ct_netconfc.erl49
-rw-r--r--lib/common_test/src/ct_property_test.erl186
-rw-r--r--lib/common_test/src/ct_release_test.erl847
7 files changed, 1140 insertions, 37 deletions
diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile
index 4600c0ad78..2723b066f0 100644
--- a/lib/common_test/src/Makefile
+++ b/lib/common_test/src/Makefile
@@ -1,7 +1,7 @@
#
# %CopyrightBegin%
#
-# Copyright Ericsson AB 2003-2013. All Rights Reserved.
+# Copyright Ericsson AB 2003-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
@@ -74,7 +74,9 @@ MODULES= \
ct_netconfc \
ct_conn_log_h \
cth_conn_log \
- ct_groups
+ ct_groups \
+ ct_property_test \
+ ct_release_test
TARGET_MODULES= $(MODULES:%=$(EBIN)/%)
BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR))
diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src
index e28751fb59..580d5dbd7b 100644
--- a/lib/common_test/src/common_test.app.src
+++ b/lib/common_test/src/common_test.app.src
@@ -1,7 +1,7 @@
% This is an -*- erlang -*- file.
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2009-2012. All Rights Reserved.
+%% Copyright Ericsson AB 2009-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
@@ -64,7 +64,7 @@
{applications, [kernel,stdlib]},
{env, []},
{runtime_dependencies,["xmerl-1.3.7","webtool-0.8.10","tools-2.6.14",
- "test_server-3.7","stdlib-2.0","ssh-3.0.1",
+ "test_server-3.7.1","stdlib-2.0","ssh-3.0.1",
"snmp-4.25.1","sasl-2.4","runtime_tools-1.8.14",
"kernel-3.0","inets-5.10","erts-6.0",
"debugger-4.0","crypto-3.3","compiler-5.0"]}]}.
diff --git a/lib/common_test/src/ct_cover.erl b/lib/common_test/src/ct_cover.erl
index cf2860ae25..c7f446dee9 100644
--- a/lib/common_test/src/ct_cover.erl
+++ b/lib/common_test/src/ct_cover.erl
@@ -128,20 +128,20 @@ get_spec(File) ->
catch get_spec_test(File).
get_spec_test(File) ->
- FullName = filename:absname(File),
- case filelib:is_file(FullName) of
+ Dir = filename:dirname(File), % always abs path in here, set in ct_run
+ case filelib:is_file(File) of
true ->
- case file:consult(FullName) of
+ case file:consult(File) of
{ok,Terms} ->
Import =
case lists:keysearch(import, 1, Terms) of
{value,{_,Imps=[S|_]}} when is_list(S) ->
ImpsFN = lists:map(fun(F) ->
- filename:absname(F)
+ filename:absname(F,Dir)
end, Imps),
test_files(ImpsFN, ImpsFN);
{value,{_,Imp=[IC|_]}} when is_integer(IC) ->
- ImpFN = filename:absname(Imp),
+ ImpFN = filename:absname(Imp,Dir),
test_files([ImpFN], [ImpFN]);
_ ->
[]
@@ -149,9 +149,9 @@ get_spec_test(File) ->
Export =
case lists:keysearch(export, 1, Terms) of
{value,{_,Exp=[EC|_]}} when is_integer(EC) ->
- filename:absname(Exp);
+ filename:absname(Exp,Dir);
{value,{_,[Exp]}} ->
- filename:absname(Exp);
+ filename:absname(Exp,Dir);
_ ->
undefined
end,
@@ -179,7 +179,7 @@ get_spec_test(File) ->
E;
[CoverSpec] ->
CoverSpec1 = remove_excludes_and_dups(CoverSpec),
- {FullName,Nodes,Import,Export,CoverSpec1};
+ {File,Nodes,Import,Export,CoverSpec1};
_ ->
{error,multiple_apps_in_cover_spec}
end;
@@ -190,7 +190,7 @@ get_spec_test(File) ->
{error,{invalid_cover_spec,Error}}
end;
false ->
- {error,{cant_read_cover_spec_file,FullName}}
+ {error,{cant_read_cover_spec_file,File}}
end.
collect_apps([{level,Level}|Ts], Apps) ->
diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl
index 43eabb18d5..7037cdca73 100644
--- a/lib/common_test/src/ct_logs.erl
+++ b/lib/common_test/src/ct_logs.erl
@@ -129,7 +129,13 @@ datestr_from_dirname([]) ->
close(Info, StartDir) ->
%% close executes on the ct_util process, not on the logger process
%% so we need to use a local copy of the log cache data
- LogCacheBin = make_last_run_index(),
+ LogCacheBin =
+ case make_last_run_index() of
+ {error,_} -> % log server not responding
+ undefined;
+ LCB ->
+ LCB
+ end,
put(ct_log_cache,LogCacheBin),
Cache2File = fun() ->
case get(ct_log_cache) of
@@ -710,6 +716,7 @@ logger_loop(State) ->
end
end,
if Importance >= (100-VLvl) ->
+ CtLogFd = State#logger_state.ct_log_fd,
case get_groupleader(Pid, GL, State) of
{tc_log,TCGL,TCGLs} ->
case erlang:is_process_alive(TCGL) of
@@ -723,14 +730,15 @@ logger_loop(State) ->
%% Group leader is dead, so write to the
%% CtLog or unexpected_io log instead
unexpected_io(Pid,Category,Importance,
- List,State),
+ List,CtLogFd),
+
logger_loop(State)
end;
{ct_log,_Fd,TCGLs} ->
%% If category is ct_internal then write
%% to ct_log, else write to unexpected_io
%% log
- unexpected_io(Pid,Category,Importance,List,State),
+ unexpected_io(Pid,Category,Importance,List,CtLogFd),
logger_loop(State#logger_state{
tc_groupleaders = TCGLs})
end;
@@ -803,16 +811,15 @@ logger_loop(State) ->
ok
end.
-create_io_fun(FromPid, State) ->
+create_io_fun(FromPid, CtLogFd) ->
%% we have to build one io-list of all strings
%% before printing, or other io printouts (made in
%% parallel) may get printed between this header
%% and footer
- Fd = State#logger_state.ct_log_fd,
fun({Str,Args}, IoList) ->
case catch io_lib:format(Str,Args) of
{'EXIT',_Reason} ->
- io:format(Fd, "Logging fails! Str: ~p, Args: ~p~n",
+ io:format(CtLogFd, "Logging fails! Str: ~p, Args: ~p~n",
[Str,Args]),
%% stop the testcase, we need to see the fault
exit(FromPid, {log_printout_error,Str,Args}),
@@ -827,28 +834,53 @@ create_io_fun(FromPid, State) ->
print_to_log(sync, FromPid, Category, TCGL, List, State) ->
%% in some situations (exceptions), the printout is made from the
%% test server IO process and there's no valid group leader to send to
+ CtLogFd = State#logger_state.ct_log_fd,
if FromPid /= TCGL ->
- IoFun = create_io_fun(FromPid, State),
+ IoFun = create_io_fun(FromPid, CtLogFd),
io:format(TCGL,"~ts", [lists:foldl(IoFun, [], List)]);
true ->
- unexpected_io(FromPid,Category,?MAX_IMPORTANCE,List,State)
+ unexpected_io(FromPid,Category,?MAX_IMPORTANCE,List,CtLogFd)
end,
State;
print_to_log(async, FromPid, Category, TCGL, List, State) ->
%% in some situations (exceptions), the printout is made from the
%% test server IO process and there's no valid group leader to send to
+ CtLogFd = State#logger_state.ct_log_fd,
Printer =
if FromPid /= TCGL ->
- IoFun = create_io_fun(FromPid, State),
+ IoFun = create_io_fun(FromPid, CtLogFd),
fun() ->
test_server:permit_io(TCGL, self()),
- io:format(TCGL, "~ts", [lists:foldl(IoFun, [], List)])
+
+ %% Since asynchronous io gets can get buffered if
+ %% the file system is slow, there is also a risk that
+ %% the group leader has terminated before we get to
+ %% the io:format(GL, ...) call. We check this and
+ %% print "expired" messages to the unexpected io
+ %% log instead (best we can do).
+
+ case erlang:is_process_alive(TCGL) of
+ true ->
+ try io:format(TCGL, "~ts",
+ [lists:foldl(IoFun,[],List)]) of
+ _ -> ok
+ catch
+ _:terminated ->
+ unexpected_io(FromPid, Category,
+ ?MAX_IMPORTANCE,
+ List, CtLogFd)
+ end;
+ false ->
+ unexpected_io(FromPid, Category,
+ ?MAX_IMPORTANCE,
+ List, CtLogFd)
+ end
end;
true ->
fun() ->
- unexpected_io(FromPid,Category,?MAX_IMPORTANCE,
- List,State)
+ unexpected_io(FromPid, Category, ?MAX_IMPORTANCE,
+ List, CtLogFd)
end
end,
case State#logger_state.async_print_jobs of
@@ -3149,12 +3181,11 @@ html_encoding(latin1) ->
html_encoding(utf8) ->
"utf-8".
-unexpected_io(Pid,ct_internal,_Importance,List,State) ->
- IoFun = create_io_fun(Pid,State),
- io:format(State#logger_state.ct_log_fd, "~ts",
- [lists:foldl(IoFun, [], List)]);
-unexpected_io(Pid,_Category,_Importance,List,State) ->
- IoFun = create_io_fun(Pid,State),
+unexpected_io(Pid,ct_internal,_Importance,List,CtLogFd) ->
+ IoFun = create_io_fun(Pid,CtLogFd),
+ io:format(CtLogFd, "~ts", [lists:foldl(IoFun, [], List)]);
+unexpected_io(Pid,_Category,_Importance,List,CtLogFd) ->
+ IoFun = create_io_fun(Pid,CtLogFd),
Data = io_lib:format("~ts", [lists:foldl(IoFun, [], List)]),
test_server_io:print_unexpected(Data),
ok.
diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl
index a3861dc745..2f66c7613c 100644
--- a/lib/common_test/src/ct_netconfc.erl
+++ b/lib/common_test/src/ct_netconfc.erl
@@ -190,6 +190,7 @@
get_config/4,
edit_config/3,
edit_config/4,
+ edit_config/5,
delete_config/2,
delete_config/3,
copy_config/3,
@@ -678,15 +679,39 @@ get_config(Client, Source, Filter, Timeout) ->
%%----------------------------------------------------------------------
%% @spec edit_config(Client, Target, Config) -> Result
-%% @equiv edit_config(Client, Target, Config, infinity)
+%% @equiv edit_config(Client, Target, Config, [], infinity)
edit_config(Client, Target, Config) ->
edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT).
%%----------------------------------------------------------------------
--spec edit_config(Client, Target, Config, Timeout) -> Result when
+-spec edit_config(Client, Target, Config, OptParamsOrTimeout) -> Result when
Client :: client(),
Target :: netconf_db(),
Config :: simple_xml(),
+ OptParamsOrTimeout :: [simple_xml()] | timeout(),
+ Result :: ok | {error,error_reason()}.
+%% @doc
+%%
+%% If `OptParamsOrTimeout' is a timeout value, then this is
+%% equivalent to {@link edit_config/5. edit_config(Client, Target,
+%% Config, [], Timeout)}.
+%%
+%% If `OptParamsOrTimeout' is a list of simple XML, then this is
+%% equivalent to {@link edit_config/5. edit_config(Client, Target,
+%% Config, OptParams, infinity)}.
+%%
+%% @end
+edit_config(Client, Target, Config, Timeout) when ?is_timeout(Timeout) ->
+ edit_config(Client, Target, Config, [], Timeout);
+edit_config(Client, Target, Config, OptParams) when is_list(OptParams) ->
+ edit_config(Client, Target, Config, OptParams, ?DEFAULT_TIMEOUT).
+
+%%----------------------------------------------------------------------
+-spec edit_config(Client, Target, Config, OptParams, Timeout) -> Result when
+ Client :: client(),
+ Target :: netconf_db(),
+ Config :: simple_xml(),
+ OptParams :: [simple_xml()],
Timeout :: timeout(),
Result :: ok | {error,error_reason()}.
%% @doc Edit configuration data.
@@ -695,10 +720,20 @@ edit_config(Client, Target, Config) ->
%% include `:candidate' or `:startup' in its list of
%% capabilities.
%%
+%% `OptParams' can be used for specifying optional parameters
+%% (`default-operation', `test-option' or `error-option') that will be
+%% added to the `edit-config' request. The value must be a list
+%% containing valid simple XML, for example
+%%
+%% ```
+%% [{'default-operation', ["none"]},
+%% {'error-option', ["rollback-on-error"]}]
+%%'''
+%%
%% @end
%%----------------------------------------------------------------------
-edit_config(Client, Target, Config, Timeout) ->
- call(Client, {send_rpc_op, edit_config, [Target,Config], Timeout}).
+edit_config(Client, Target, Config, OptParams, Timeout) ->
+ call(Client, {send_rpc_op, edit_config, [Target,Config,OptParams], Timeout}).
%%----------------------------------------------------------------------
@@ -1086,6 +1121,7 @@ handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) ->
SimpleXml = encode_rpc_operation(get,[Filter]),
do_send_rpc(Op, SimpleXml, Timeout, From, State).
+%% @private
handle_msg({ssh_cm, CM, {data, Ch, _Type, Data}}, State) ->
ssh_connection:adjust_window(CM,Ch,size(Data)),
handle_data(Data, State);
@@ -1234,8 +1270,8 @@ encode_rpc_operation(get,[Filter]) ->
{get,filter(Filter)};
encode_rpc_operation(get_config,[Source,Filter]) ->
{'get-config',[{source,[Source]}] ++ filter(Filter)};
-encode_rpc_operation(edit_config,[Target,Config]) ->
- {'edit-config',[{target,[Target]},{config,[Config]}]};
+encode_rpc_operation(edit_config,[Target,Config,OptParams]) ->
+ {'edit-config',[{target,[Target]}] ++ OptParams ++ [{config,[Config]}]};
encode_rpc_operation(delete_config,[Target]) ->
{'delete-config',[{target,[Target]}]};
encode_rpc_operation(copy_config,[Target,Source]) ->
@@ -1707,6 +1743,7 @@ log(#connection{host=Host,port=Port,name=Name},Action,Data) ->
%% Log callback - called from the error handler process
+%% @private
format_data(How,Data) ->
%% Assuming that the data is encoded as UTF-8. If it is not, then
%% the printout might be wrong, but the format function will not
diff --git a/lib/common_test/src/ct_property_test.erl b/lib/common_test/src/ct_property_test.erl
new file mode 100644
index 0000000000..52acda5388
--- /dev/null
+++ b/lib/common_test/src/ct_property_test.erl
@@ -0,0 +1,186 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2003-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%
+%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% WARNING %%%
+%%% %%%
+%%% This is experimental code which may be changed or removed %%%
+%%% anytime without any warning. %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+%%% @doc EXPERIMENTAL support in common-test for calling property based tests.
+%%%
+%%% <p>This module is a first step towards running Property Based testing in the
+%%% Common Test framework. A property testing tool like QuickCheck or PropEr is
+%%% assumed to be installed.</p>
+%%%
+%%% <p>The idea is to have a common_test testsuite calling a property testing
+%%% tool with special property test suites as defined by that tool. In this manual
+%%% we assume the usual Erlang Application directory structure. The tests are
+%%% collected in the application's <c>test</c> directory. The test directory
+%%% has a sub-directory called <c>property_test</c> where everything needed for
+%%% the property tests are collected.</p>
+%%%
+%%% <p>A typical ct test suite using <c>ct_property_test</c> is organized as follows:
+%%% </p>
+%%% ```
+%%% -include_lib("common_test/include/ct.hrl").
+%%%
+%%% all() -> [prop_ftp_case].
+%%%
+%%% init_per_suite(Config) ->
+%%% ct_property_test:init_per_suite(Config).
+%%%
+%%% %%%---- test case
+%%% prop_ftp_case(Config) ->
+%%% ct_property_test:quickcheck(
+%%% ftp_simple_client_server:prop_ftp(Config),
+%%% Config
+%%% ).
+%%% '''
+%%%
+%%% <warning>
+%%% <p>
+%%% This is experimental code which may be changed or removed
+%%% anytime without any warning.
+%%% </p>
+%%% </warning>
+%%%
+%%% @end
+
+-module(ct_property_test).
+
+%% API
+-export([init_per_suite/1,
+ quickcheck/2]).
+
+-include_lib("common_test/include/ct.hrl").
+
+%%%-----------------------------------------------------------------
+%%% @spec init_per_suite(Config) -> Config | {skip,Reason}
+%%%
+%%% @doc Initializes Config for property testing.
+%%%
+%%% <p>The function investigates if support is available for either Quickcheck, PropEr,
+%%% or Triq.
+%%% The options <c>{property_dir,AbsPath}</c> and
+%%% <c>{property_test_tool,Tool}</c> is set in the Config returned.</p>
+%%% <p>The function is intended to be called in the init_per_suite in the test suite.</p>
+%%% <p>The property tests are assumed to be in the subdirectory <c>property_test</c>.</p>
+%%% @end
+
+init_per_suite(Config) ->
+ case which_module_exists([eqc,proper,triq]) of
+ {ok,ToolModule} ->
+ ct:pal("Found property tester ~p",[ToolModule]),
+ Path = property_tests_path("property_test", Config),
+ case compile_tests(Path,ToolModule) of
+ error ->
+ {fail, "Property test compilation failed in "++Path};
+ up_to_date ->
+ add_code_pathz(Path),
+ [{property_dir,Path},
+ {property_test_tool,ToolModule} | Config]
+ end;
+
+ not_found ->
+ ct:pal("No property tester found",[]),
+ {skip, "No property testing tool found"}
+ end.
+
+%%%-----------------------------------------------------------------
+%%% @spec quickcheck(Property, Config) -> true | {fail,Reason}
+%%%
+%%% @doc Call quickcheck and return the result in a form suitable for common_test.
+%%%
+%%% <p>The function is intended to be called in the test cases in the test suite.</p>
+%%% @end
+
+quickcheck(Property, Config) ->
+ Tool = proplists:get_value(property_test_tool,Config),
+ F = function_name(quickcheck, Tool),
+ mk_ct_return( Tool:F(Property), Tool ).
+
+
+%%%================================================================
+%%%
+%%% Local functions
+%%%
+
+%%% Make return values back to the calling Common Test suite
+mk_ct_return(true, _Tool) ->
+ true;
+mk_ct_return(Other, Tool) ->
+ try lists:last(hd(Tool:counterexample()))
+ of
+ {set,{var,_},{call,M,F,Args}} ->
+ {fail, io_lib:format("~p:~p/~p returned bad result",[M,F,length(Args)])}
+ catch
+ _:_ ->
+ {fail, Other}
+ end.
+
+%%% Check if a property testing tool is found
+which_module_exists([Module|Modules]) ->
+ case module_exists(Module) of
+ true -> {ok,Module};
+ false -> which_module_exists(Modules)
+ end;
+which_module_exists(_) ->
+ not_found.
+
+module_exists(Module) ->
+ is_list(catch Module:module_info()).
+
+%%% The path to the property tests
+property_tests_path(Dir, Config) ->
+ DataDir = proplists:get_value(data_dir, Config),
+ filename:join(lists:droplast(filename:split(DataDir))++[Dir]).
+
+%%% Extend the code path with Dir if it not already present
+add_code_pathz(Dir) ->
+ case lists:member(Dir, code:get_path()) of
+ true -> ok;
+ false -> code:add_pathz(Dir)
+ end.
+
+compile_tests(Path, ToolModule) ->
+ MacroDefs = macro_def(ToolModule),
+ {ok,Cwd} = file:get_cwd(),
+ ok = file:set_cwd(Path),
+ {ok,FileNames} = file:list_dir("."),
+ BeamFiles = [F || F<-FileNames,
+ filename:extension(F) == ".beam"],
+ [file:delete(F) || F<-BeamFiles],
+ ct:pal("Compiling in ~p:~n Deleted ~p~n MacroDefs=~p",[Path,BeamFiles,MacroDefs]),
+ Result = make:all([load|MacroDefs]),
+ file:set_cwd(Cwd),
+ Result.
+
+
+macro_def(eqc) -> [{d, 'EQC'}];
+macro_def(proper) -> [{d, 'PROPER'}];
+macro_def(triq) -> [{d, 'TRIQ'}].
+
+function_name(quickcheck, triq) -> check;
+function_name(F, _) -> F.
+
diff --git a/lib/common_test/src/ct_release_test.erl b/lib/common_test/src/ct_release_test.erl
new file mode 100644
index 0000000000..eb9e9c832f
--- /dev/null
+++ b/lib/common_test/src/ct_release_test.erl
@@ -0,0 +1,847 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 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%
+%%
+%%-----------------------------------------------------------------
+%% @doc EXPERIMENTAL support for testing of upgrade.
+%%
+%% This is a library module containing support for test of release
+%% related activities in one or more applications. Currenty it
+%% supports upgrade only.
+%%
+%% == Configuration ==
+%%
+%% In order to find version numbers of applications to upgrade from,
+%% `{@module}' needs to access and start old OTP
+%% releases. A `common_test' configuration file can be used for
+%% specifying the location of such releases, for example:
+%%
+%% ```
+%% %% old-rels.cfg
+%% {otp_releases,[{r16b,"/path/to/R16B03-1/bin/erl"},
+%% {'17',"/path/to/17.3/bin/erl"}]}.'''
+%%
+%% The configuration file should preferably point out the latest patch
+%% level on each major release.
+%%
+%% If no such configuration file is given, {@link init/1} will return
+%% `{skip,Reason}' and any attempt at running {@link upgrade/4}
+%% will fail.
+%%
+%% == Callback functions ==
+%%
+%% The following functions should be exported from a {@module}
+%% callback module.
+%%
+%% All callback functions are called on the node where the upgrade is
+%% executed.
+%%
+%% <dl>
+%% <dt>Module:upgrade_init(State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><c>State = NewState = cb_state()</c></b>
+%%
+%% Initialyze system before upgrade test starts.
+%%
+%% This function is called before the upgrade is started. All
+%% applications given in {@link upgrade/4} are already started by
+%% the boot script, so this callback is intended for additional
+%% initialization, if necessary.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_init(State) ->
+%% open_connection(State).'''
+%% </dd>
+%%
+%% <dt>Module:upgrade_upgraded(State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><c>State = NewState = cb_state()</c></b>
+%%
+%% Check that upgrade was successful.
+%%
+%% This function is called after the release_handler has
+%% successfully unpacked and installed the new release, and it has
+%% been made permanent. It allows application specific checks to
+%% ensure that the upgrade was successful.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_upgraded(State) ->
+%% check_connection_still_open(State).'''
+%% </dd>
+%%
+%% <dt>Module:upgrade_downgraded(State) -> NewState</dt>
+%% <dd>Types:
+%%
+%% <b><c>State = NewState = cb_state()</c></b>
+%%
+%% Check that downgrade was successful.
+%%
+%% This function is called after the release_handler has
+%% successfully re-installed the original release, and it has been
+%% made permanent. It allows application specific checks to ensure
+%% that the downgrade was successful.
+%%
+%% Example:
+%%
+%% ```
+%% upgrade_init(State) ->
+%% check_connection_closed(State).'''
+%% </dd>
+%% </dl>
+%% @end
+%%-----------------------------------------------------------------
+-module(ct_release_test).
+
+-export([init/1, upgrade/4, cleanup/1]).
+
+-include_lib("kernel/include/file.hrl").
+
+%%-----------------------------------------------------------------
+-define(testnode, otp_upgrade).
+-define(exclude_apps, [hipe, typer, dialyzer]). % never include these apps
+
+%%-----------------------------------------------------------------
+-type config() :: [{atom(),term()}].
+-type cb_state() :: term().
+
+-callback upgrade_init(cb_state()) -> cb_state().
+-callback upgrade_upgraded(cb_state()) -> cb_state().
+-callback upgrade_downgraded(cb_state()) -> cb_state().
+
+%%-----------------------------------------------------------------
+-spec init(Config) -> Result when
+ Config :: config(),
+ Result :: config() | SkipOrFail,
+ SkipOrFail :: {skip,Reason} | {fail,Reason}.
+%% @doc Initialize `{@module}'.
+%%
+%% This function can be called from any of the
+%% `init_per_*' functions in the test suite. It updates
+%% the given `Config' with data that will be
+%% used by future calls to other functions in this module. The
+%% returned configuration must therefore also be returned from
+%% the calling `init_per_*'.
+%%
+%% If the initialization fails, e.g. if a required release can
+%% not be found, the function returns `{skip,Reason}'. In
+%% this case the other test support functions in this mudule
+%% can not be used.
+%%
+%% Example:
+%%
+%% ```
+%% init_per_suite(Config) ->
+%% ct_release_test:init(Config).'''
+%%
+init(Config) ->
+ try init_upgrade_test() of
+ {Major,Minor} ->
+ [{release_test,[{major,Major},{minor,Minor}]} | Config]
+ catch throw:Thrown ->
+ Thrown
+ end.
+
+%%-----------------------------------------------------------------
+-spec upgrade(App,Level,Callback,Config) -> any() when
+ App :: atom(),
+ Level :: minor | major,
+ Callback :: {module(),InitState},
+ InitState :: cb_state(),
+ Config :: config();
+ (Apps,Level,Callback,Config) -> any() when
+ Apps :: [App],
+ App :: atom(),
+ Level :: minor | major,
+ Callback :: {module(),InitState},
+ InitState :: cb_state(),
+ Config :: config().
+%% @doc Test upgrade of the given application(s).
+%%
+%% This function can be called from a test case. It requires that
+%% `Config' has been initialized by calling {@link
+%% init/1} prior to this, for example from `init_per_suite/1'.
+%%
+%% Upgrade tests are performed as follows:
+%%
+%% <ol>
+%% <li>Figure out which OTP release to test upgrade
+%% from. Start a node running that release and find the
+%% application versions on that node. Terminate the
+%% node.</li>
+%% <li>Figure out all dependencies for the applications under
+%% test.</li>
+%% <li>Create a release containing the core
+%% applications `kernel', `stdlib' and `sasl'
+%% in addition to the application(s) under test and all
+%% dependencies of these. The versions of the applications
+%% under test will be the ones found on the OTP release to
+%% upgrade from. The versions of all other applications will
+%% be those found on the current node, i.e. the common_test
+%% node. This is the "From"-release.</li>
+%% <li>Create another release containing the same
+%% applications as in the previous step, but with all
+%% application versions taken from the current node. This is
+%% the "To"-release.</li>
+%% <li>Install the "From"-release and start a new node
+%% running this release.</li>
+%% <li>Perform the upgrade test and allow customized
+%% control by using callbacks:
+%% <ol>
+%% <li>Callback: `upgrade_init/1'</li>
+%% <li>Unpack the new release</li>
+%% <li>Install the new release</li>
+%% <li>Callback: `upgrade_upgraded/1'</li>
+%% <li>Install the original release</li>
+%% <li>Callback: `upgrade_downgraded/1'</li>
+%% </ol>
+%% </li>
+%% </ol>
+%%
+%% `App' or `Apps'
+%% specifies the applications under test, i.e. the applications
+%% which shall be upgraded. All other applications that are
+%% included have the same releases in the "From"- and
+%% "To"-releases and will therefore not be upgraded.
+%%
+%% `Level' specifies which OTP release to
+%% pick the "From" versions from.
+%% <dl>
+%% <dt>major</dt>
+%% <dd>From verions are picked from the previous major
+%% release. For example, if the test is run on an OTP-17
+%% node, `{@module}' will pick the application
+%% "From" versions from an OTP installation running OTP
+%% R16B.</dd>
+%%
+%% <dt>minor</dt>
+%% <dd>From verions are picked from the current major
+%% release. For example, if the test is run on an OTP-17
+%% node, `{@module}' will pick the application
+%% "From" versions from an OTP installation running an
+%% earlier patch level of OTP-17.</dd>
+%% </dl>
+%%
+%% The application "To" versions are allways picked from the
+%% current node, i.e. the common_test node.
+%%
+%% `Callback' specifies the module (normally the
+%% test suite) which implements the {@section Callback functions}, and
+%% the initial value of the `State' variable used in these
+%% functions.
+%%
+%% `Config' is the input argument received
+%% in the test case function.
+%%
+%% Example:
+%%
+%% ```
+%% minor_upgrade(Config) ->
+%% ct_release_test:upgrade(ssl,minor,{?MODULE,[]},Config).
+%% '''
+%%
+upgrade(App,Level,Callback,Config) when is_atom(App) ->
+ upgrade([App],Level,Callback,Config);
+upgrade(Apps,Level,Callback,Config) ->
+ Dir = proplists:get_value(priv_dir,Config),
+ CreateDir = filename:join([Dir,Level,create]),
+ InstallDir = filename:join([Dir,Level,install]),
+ ok = filelib:ensure_dir(filename:join(CreateDir,"*")),
+ ok = filelib:ensure_dir(filename:join(InstallDir,"*")),
+ try upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) of
+ ok ->
+ %%rm_rf(CreateDir),
+ Tars = filelib:wildcard(filename:join(CreateDir,"*.tar.gz")),
+ _ = [file:delete(Tar) || Tar <- Tars],
+ rm_rf(InstallDir),
+ ok
+ catch throw:{fail,Reason} ->
+ ct:fail(Reason);
+ throw:{skip,Reason} ->
+ rm_rf(CreateDir),
+ rm_rf(InstallDir),
+ {skip,Reason}
+ after
+ %% Brutally kill all nodes that erroneously survived the test.
+ %% Note, we will not reach this if the test fails with a
+ %% timetrap timeout in the test suite! Thus we can have
+ %% hanging nodes...
+ Nodes = nodes(),
+ [rpc:call(Node,erlang,halt,[]) || Node <- Nodes]
+ end.
+
+%%-----------------------------------------------------------------
+-spec cleanup(Config) -> Result when
+ Config :: config(),
+ Result :: config().
+%% @doc Clean up after tests.
+%%
+%% This function shall be called from the `end_per_*' function
+%% complementing the `init_per_*' function where {@link init/1}
+%% is called.
+%%
+%% It cleans up after the test, for example kills hanging
+%% nodes.
+%%
+%% Example:
+%%
+%% ```
+%% end_per_suite(Config) ->
+%% ct_release_test:cleanup(Config).'''
+%%
+cleanup(Config) ->
+ Nodes = [node_name(?testnode)|nodes()],
+ [rpc:call(Node,erlang,halt,[]) || Node <- Nodes],
+ Config.
+
+%%-----------------------------------------------------------------
+init_upgrade_test() ->
+ %% Check that a real release is running, not e.g. cerl
+ ok = application:ensure_started(sasl),
+ case release_handler:which_releases() of
+ [{_,_,[],_}] ->
+ %% Fake release, no applications
+ throw({skip, "Need a real release running to create other releases"});
+ _ ->
+ Major = init_upgrade_test(major),
+ Minor = init_upgrade_test(minor),
+ {Major,Minor}
+ end.
+
+init_upgrade_test(Level) ->
+ {FromVsn,ToVsn} = get_rels(Level),
+ OldRel =
+ case test_server:is_release_available(FromVsn) of
+ true ->
+ {release,FromVsn};
+ false ->
+ case ct:get_config({otp_releases,list_to_atom(FromVsn)}) of
+ undefined ->
+ false;
+ Prog0 ->
+ case os:find_executable(Prog0) of
+ false ->
+ false;
+ Prog ->
+ {prog,Prog}
+ end
+ end
+ end,
+ case OldRel of
+ false ->
+ ct:log("Release ~p is not available."
+ " Upgrade on '~p' level can not be tested.",
+ [FromVsn,Level]),
+ undefined;
+ _ ->
+ init_upgrade_test(FromVsn,ToVsn,OldRel)
+ end.
+
+get_rels(major) ->
+ %% Given that the current major release is X, then this is an
+ %% upgrade from major release X-1 to the current release.
+ Current = erlang:system_info(otp_release),
+ PreviousMajor = previous_major(Current),
+ {PreviousMajor,Current};
+get_rels(minor) ->
+ %% Given that this is a (possibly) patched version of major
+ %% release X, then this is an upgrade from major release X to the
+ %% current release.
+ CurrentMajor = erlang:system_info(otp_release),
+ Current = CurrentMajor++"_patched",
+ {CurrentMajor,Current}.
+
+init_upgrade_test(FromVsn,ToVsn,OldRel) ->
+ OtpRel = list_to_atom("otp-"++FromVsn),
+ ct:log("Starting node to fetch application versions to upgrade from"),
+ {ok,Node} = test_server:start_node(OtpRel,peer,[{erl,[OldRel]}]),
+ {Apps,Path} = fetch_all_apps(Node),
+ test_server:stop_node(Node),
+ {FromVsn,ToVsn,Apps,Path}.
+
+fetch_all_apps(Node) ->
+ Paths = rpc:call(Node,code,get_path,[]),
+ %% Find all possible applications in the path
+ AppFiles =
+ lists:flatmap(
+ fun(P) ->
+ filelib:wildcard(filename:join(P,"*.app"))
+ end,
+ Paths),
+ %% Figure out which version of each application is running on this
+ %% node. Using application:load and application:get_key instead of
+ %% reading the .app files since there might be multiple versions
+ %% of a .app file and we only want the one that is actually
+ %% running.
+ AppVsns =
+ lists:flatmap(
+ fun(F) ->
+ A = list_to_atom(filename:basename(filename:rootname(F))),
+ _ = rpc:call(Node,application,load,[A]),
+ case rpc:call(Node,application,get_key,[A,vsn]) of
+ {ok,V} -> [{A,V}];
+ _ -> []
+ end
+ end,
+ AppFiles),
+ ErtsVsn = rpc:call(Node, erlang, system_info, [version]),
+ {[{erts,ErtsVsn}|AppVsns], Paths}.
+
+
+%%-----------------------------------------------------------------
+upgrade(Apps,Level,Callback,CreateDir,InstallDir,Config) ->
+ ct:log("Test upgrade of the following applications: ~p",[Apps]),
+ ct:log(".rel files and start scripts are created in:~n~ts",[CreateDir]),
+ ct:log("The release is installed in:~n~ts",[InstallDir]),
+ case proplists:get_value(release_test,Config) of
+ undefined ->
+ throw({fail,"ct_release_test:init/1 not run"});
+ RTConfig ->
+ case proplists:get_value(Level,RTConfig) of
+ undefined ->
+ throw({skip,"Old release not available"});
+ Data ->
+ {FromVsn,FromRel,FromAppsVsns} =
+ target_system(Apps, CreateDir, InstallDir, Data),
+ {ToVsn,ToRel,ToAppsVsns} =
+ upgrade_system(Apps, FromRel, CreateDir,
+ InstallDir, Data),
+ ct:log("Upgrade from: OTP-~ts, ~p",[FromVsn, FromAppsVsns]),
+ ct:log("Upgrade to: OTP-~ts, ~p",[ToVsn, ToAppsVsns]),
+ do_upgrade(Callback, FromVsn, FromAppsVsns, ToRel,
+ ToAppsVsns, InstallDir)
+ end
+ end.
+
+%%% This is similar to sasl/examples/src/target_system.erl, but with
+%%% the following adjustments:
+%%% - add a log directory
+%%% - use an own 'start' script
+%%% - chmod 'start' and 'start_erl'
+target_system(Apps,CreateDir,InstallDir,{FromVsn,_,AllAppsVsns,Path}) ->
+ RelName0 = "otp-"++FromVsn,
+
+ AppsVsns = [{A,V} || {A,V} <- AllAppsVsns, lists:member(A,Apps)],
+ {RelName,ErtsVsn} = create_relfile(AppsVsns,CreateDir,RelName0,FromVsn),
+
+ %% Create .script and .boot
+ ok = systools(make_script,[RelName,[{path,Path}]]),
+
+ %% Create base tar file - i.e. erts and all apps
+ ok = systools(make_tar,[RelName,[{erts,code:root_dir()},
+ {path,Path}]]),
+
+ %% Unpack the tar to complete the installation
+ erl_tar:extract(RelName ++ ".tar.gz", [{cwd, InstallDir}, compressed]),
+
+ %% Add bin and log dirs
+ BinDir = filename:join([InstallDir, "bin"]),
+ file:make_dir(BinDir),
+ file:make_dir(filename:join(InstallDir,"log")),
+
+ %% Delete start scripts - they will be added later
+ ErtsBinDir = filename:join([InstallDir, "erts-" ++ ErtsVsn, "bin"]),
+ file:delete(filename:join([ErtsBinDir, "erl"])),
+ file:delete(filename:join([ErtsBinDir, "start"])),
+ file:delete(filename:join([ErtsBinDir, "start_erl"])),
+
+ %% Copy .boot to bin/start.boot
+ copy_file(RelName++".boot",filename:join([BinDir, "start.boot"])),
+
+ %% Copy scripts from erts-xxx/bin to bin
+ copy_file(filename:join([ErtsBinDir, "epmd"]),
+ filename:join([BinDir, "epmd"]), [preserve]),
+ copy_file(filename:join([ErtsBinDir, "run_erl"]),
+ filename:join([BinDir, "run_erl"]), [preserve]),
+ copy_file(filename:join([ErtsBinDir, "to_erl"]),
+ filename:join([BinDir, "to_erl"]), [preserve]),
+
+ %% create start_erl.data, sys.config and start.src
+ StartErlData = filename:join([InstallDir, "releases", "start_erl.data"]),
+ write_file(StartErlData, io_lib:fwrite("~s ~s~n", [ErtsVsn, FromVsn])),
+ SysConfig = filename:join([InstallDir, "releases", FromVsn, "sys.config"]),
+ write_file(SysConfig, "[]."),
+ StartSrc = filename:join(ErtsBinDir,"start.src"),
+ write_file(StartSrc,start_script()),
+ ok = file:change_mode(StartSrc,8#0755),
+
+ %% Make start_erl executable
+ %% (this has been fixed in OTP 17 - it is now installed with
+ %% $INSTALL_SCRIPT instead of $INSTALL_DATA and should therefore
+ %% be executable from the start)
+ ok = file:change_mode(filename:join(ErtsBinDir,"start_erl.src"),8#0755),
+
+ %% Substitute variables in erl.src, start.src and start_erl.src
+ %% (.src found in erts-xxx/bin - result stored in bin)
+ subst_src_scripts(["erl", "start", "start_erl"], ErtsBinDir, BinDir,
+ [{"FINAL_ROOTDIR", InstallDir}, {"EMU", "beam"}],
+ [preserve]),
+
+ %% Create RELEASES
+ RelFile = filename:join([InstallDir, "releases",
+ filename:basename(RelName) ++ ".rel"]),
+ release_handler:create_RELEASES(InstallDir, RelFile),
+
+ {FromVsn, RelName,AppsVsns}.
+
+systools(Func,Args) ->
+ case apply(systools,Func,Args) of
+ ok ->
+ ok;
+ error ->
+ throw({fail,{systools,Func,Args}})
+ end.
+
+%%% This is a copy of $ROOT/erts-xxx/bin/start.src, modified to add
+%%% sname and heart
+start_script() ->
+ ["#!/bin/sh\n"
+ "ROOTDIR=%FINAL_ROOTDIR%\n"
+ "\n"
+ "if [ -z \"$RELDIR\" ]\n"
+ "then\n"
+ " RELDIR=$ROOTDIR/releases\n"
+ "fi\n"
+ "\n"
+ "START_ERL_DATA=${1:-$RELDIR/start_erl.data}\n"
+ "\n"
+ "$ROOTDIR/bin/run_erl -daemon /tmp/ $ROOTDIR/log \"exec $ROOTDIR/bin/start_erl $ROOTDIR $RELDIR $START_ERL_DATA -sname ",atom_to_list(?testnode)," -heart\"\n"].
+
+%%% Create a release containing the current (the test node) OTP
+%%% release, including relup to allow upgrade from an earlier OTP
+%%% release.
+upgrade_system(Apps, FromRel, CreateDir, InstallDir, {_,ToVsn,_,_}) ->
+ ct:log("Generating release to upgrade to."),
+
+ RelName0 = "otp-"++ToVsn,
+
+ AppsVsns = get_vsns(Apps),
+ {RelName,_} = create_relfile(AppsVsns,CreateDir,RelName0,ToVsn),
+ FromPath = filename:join([InstallDir,lib,"*",ebin]),
+
+ ok = systools(make_script,[RelName]),
+ ok = systools(make_relup,[RelName,[FromRel],[FromRel],
+ [{path,[FromPath]},
+ {outdir,CreateDir}]]),
+ SysConfig = filename:join([CreateDir, "sys.config"]),
+ write_file(SysConfig, "[]."),
+
+ ok = systools(make_tar,[RelName,[{erts,code:root_dir()}]]),
+
+ {ToVsn, RelName,AppsVsns}.
+
+%%% Start a new node running the release from target_system/6
+%%% above. Then upgrade to the system from upgrade_system/6.
+do_upgrade({Cb,InitState},FromVsn,FromAppsVsns,ToRel,ToAppsVsns,InstallDir) ->
+ ct:log("Upgrade test attempting to start node.~n"
+ "If test fails, logs can be found in:~n~ts",
+ [filename:join(InstallDir,log)]),
+ Start = filename:join([InstallDir,bin,start]),
+ {ok,Node} = start_node(Start,FromVsn,FromAppsVsns),
+
+ ct:log("Node started: ~p",[Node]),
+ State1 = do_callback(Node,Cb,upgrade_init,InitState),
+
+ [{"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ToRelName = filename:basename(ToRel),
+ copy_file(ToRel++".tar.gz",
+ filename:join([InstallDir,releases,ToRelName++".tar.gz"])),
+ ct:log("Unpacking new release"),
+ {ok,ToVsn} = rpc:call(Node,release_handler,unpack_release,[ToRelName]),
+ [{"OTP upgrade test",ToVsn,_,unpacked},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Installing new release"),
+ case rpc:call(Node,release_handler,install_release,[ToVsn]) of
+ {ok,FromVsn,_} ->
+ ok;
+ {continue_after_restart,FromVsn,_} ->
+ ct:log("Waiting for node restart")
+ end,
+ %% even if install_release returned {ok,...} there might be an
+ %% emulator restart (instruction restart_emulator), so we must
+ %% always make sure the node is running.
+ wait_node_up(current,ToVsn,ToAppsVsns),
+
+ [{"OTP upgrade test",ToVsn,_,current},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Permanenting new release"),
+ ok = rpc:call(Node,release_handler,make_permanent,[ToVsn]),
+ [{"OTP upgrade test",ToVsn,_,permanent},
+ {"OTP upgrade test",FromVsn,_,old}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+
+ State2 = do_callback(Node,Cb,upgrade_upgraded,State1),
+
+ ct:log("Re-installing old release"),
+ case rpc:call(Node,release_handler,install_release,[FromVsn]) of
+ {ok,FromVsn,_} ->
+ ok;
+ {continue_after_restart,FromVsn,_} ->
+ ct:log("Waiting for node restart")
+ end,
+ %% even if install_release returned {ok,...} there might be an
+ %% emulator restart (instruction restart_emulator), so we must
+ %% always make sure the node is running.
+ wait_node_up(current,FromVsn,FromAppsVsns),
+
+ [{"OTP upgrade test",ToVsn,_,permanent},
+ {"OTP upgrade test",FromVsn,_,current}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+ ct:log("Permanenting old release"),
+ ok = rpc:call(Node,release_handler,make_permanent,[FromVsn]),
+ [{"OTP upgrade test",ToVsn,_,old},
+ {"OTP upgrade test",FromVsn,_,permanent}] =
+ rpc:call(Node,release_handler,which_releases,[]),
+
+ _State3 = do_callback(Node,Cb,upgrade_downgraded,State2),
+
+ ct:log("Terminating node ~p",[Node]),
+ erlang:monitor_node(Node,true),
+ _ = rpc:call(Node,init,stop,[]),
+ receive {nodedown,Node} -> ok end,
+ ct:log("Node terminated"),
+
+ ok.
+
+do_callback(Node,Mod,Func,State) ->
+ Dir = filename:dirname(code:which(Mod)),
+ _ = rpc:call(Node,code,add_path,[Dir]),
+ ct:log("Calling ~p:~p/1",[Mod,Func]),
+ R = rpc:call(Node,Mod,Func,[State]),
+ ct:log("~p:~p/1 returned: ~p",[Mod,Func,R]),
+ case R of
+ {badrpc,Error} ->
+ test_server:fail({test_upgrade_callback,Mod,Func,State,Error});
+ NewState ->
+ NewState
+ end.
+
+%%% Library functions
+previous_major("17") ->
+ "r16b";
+previous_major(Rel) ->
+ integer_to_list(list_to_integer(Rel)-1).
+
+create_relfile(AppsVsns,CreateDir,RelName0,RelVsn) ->
+ UpgradeAppsVsns = [{A,V,restart_type(A)} || {A,V} <- AppsVsns],
+
+ CoreAppVsns0 = get_vsns([kernel,stdlib,sasl]),
+ CoreAppVsns =
+ [{A,V,restart_type(A)} || {A,V} <- CoreAppVsns0,
+ false == lists:keymember(A,1,AppsVsns)],
+
+ Apps = [App || {App,_} <- AppsVsns],
+ StartDepsVsns = get_start_deps(Apps,CoreAppVsns),
+ StartApps = [StartApp || {StartApp,_,_} <- StartDepsVsns] ++ Apps,
+
+ {RuntimeDepsVsns,_} = get_runtime_deps(StartApps,StartApps,[],[]),
+
+ AllAppsVsns0 = StartDepsVsns ++ UpgradeAppsVsns ++ RuntimeDepsVsns,
+
+ %% Should test tools really be included? Some library functions
+ %% here could be used by callback, but not everything since
+ %% processes of these applications will not be running.
+ TestToolAppsVsns0 = get_vsns([test_server,common_test]),
+ TestToolAppsVsns =
+ [{A,V,none} || {A,V} <- TestToolAppsVsns0,
+ false == lists:keymember(A,1,AllAppsVsns0)],
+
+ AllAppsVsns1 = AllAppsVsns0 ++ TestToolAppsVsns,
+ AllAppsVsns = [AV || AV={A,_,_} <- AllAppsVsns1,
+ false == lists:member(A,?exclude_apps)],
+
+ ErtsVsn = erlang:system_info(version),
+
+ %% Create the .rel file
+ RelContent = {release,{"OTP upgrade test",RelVsn},{erts,ErtsVsn},AllAppsVsns},
+ RelName = filename:join(CreateDir,RelName0),
+ RelFile = RelName++".rel",
+ {ok,Fd} = file:open(RelFile,[write,{encoding,utf8}]),
+ io:format(Fd,"~tp.~n",[RelContent]),
+ ok = file:close(Fd),
+ {RelName,ErtsVsn}.
+
+get_vsns(Apps) ->
+ [begin
+ _ = application:load(A),
+ {ok,V} = application:get_key(A,vsn),
+ {A,V}
+ end || A <- Apps].
+
+get_start_deps([App|Apps],Acc) ->
+ _ = application:load(App),
+ {ok,StartDeps} = application:get_key(App,applications),
+ StartDepsVsns =
+ [begin
+ _ = application:load(StartApp),
+ {ok,StartVsn} = application:get_key(StartApp,vsn),
+ {StartApp,StartVsn,restart_type(StartApp)}
+ end || StartApp <- StartDeps,
+ false == lists:keymember(StartApp,1,Acc)],
+ DepsStartDeps = get_start_deps(StartDeps,Acc ++ StartDepsVsns),
+ get_start_deps(Apps,DepsStartDeps);
+get_start_deps([],Acc) ->
+ Acc.
+
+get_runtime_deps([App|Apps],StartApps,Acc,Visited) ->
+ case lists:member(App,Visited) of
+ true ->
+ get_runtime_deps(Apps,StartApps,Acc,Visited);
+ false ->
+ %% runtime_dependencies should be possible to read with
+ %% application:get_key/2, but still isn't so we need to
+ %% read the .app file...
+ AppFile = code:where_is_file(atom_to_list(App) ++ ".app"),
+ {ok,[{application,App,Attrs}]} = file:consult(AppFile),
+ RuntimeDeps =
+ lists:flatmap(
+ fun(Str) ->
+ [RuntimeAppStr,_] = string:tokens(Str,"-"),
+ RuntimeApp = list_to_atom(RuntimeAppStr),
+ case {lists:keymember(RuntimeApp,1,Acc),
+ lists:member(RuntimeApp,StartApps)} of
+ {false,false} when RuntimeApp=/=erts ->
+ [RuntimeApp];
+ _ ->
+ []
+ end
+ end,
+ proplists:get_value(runtime_dependencies,Attrs,[])),
+ RuntimeDepsVsns =
+ [begin
+ _ = application:load(RuntimeApp),
+ {ok,RuntimeVsn} = application:get_key(RuntimeApp,vsn),
+ {RuntimeApp,RuntimeVsn,none}
+ end || RuntimeApp <- RuntimeDeps],
+ {DepsRuntimeDeps,NewVisited} =
+ get_runtime_deps(RuntimeDeps,StartApps,Acc++RuntimeDepsVsns,[App|Visited]),
+ get_runtime_deps(Apps,StartApps,DepsRuntimeDeps,NewVisited)
+ end;
+get_runtime_deps([],_,Acc,Visited) ->
+ {Acc,Visited}.
+
+restart_type(App) when App==kernel; App==stdlib; App==sasl ->
+ permanent;
+restart_type(_) ->
+ temporary.
+
+copy_file(Src, Dest) ->
+ copy_file(Src, Dest, []).
+
+copy_file(Src, Dest, Opts) ->
+ {ok,_} = file:copy(Src, Dest),
+ case lists:member(preserve, Opts) of
+ true ->
+ {ok, FileInfo} = file:read_file_info(Src),
+ file:write_file_info(Dest, FileInfo);
+ false ->
+ ok
+ end.
+
+write_file(FName, Conts) ->
+ Enc = file:native_name_encoding(),
+ {ok, Fd} = file:open(FName, [write]),
+ file:write(Fd, unicode:characters_to_binary(Conts,Enc,Enc)),
+ file:close(Fd).
+
+%% Substitute all occurrences of %Var% for Val in the given scripts
+subst_src_scripts(Scripts, SrcDir, DestDir, Vars, Opts) ->
+ lists:foreach(fun(Script) ->
+ subst_src_script(Script, SrcDir, DestDir,
+ Vars, Opts)
+ end, Scripts).
+
+subst_src_script(Script, SrcDir, DestDir, Vars, Opts) ->
+ subst_file(filename:join([SrcDir, Script ++ ".src"]),
+ filename:join([DestDir, Script]),
+ Vars, Opts).
+
+subst_file(Src, Dest, Vars, Opts) ->
+ {ok, Bin} = file:read_file(Src),
+ Conts = binary_to_list(Bin),
+ NConts = subst(Conts, Vars),
+ write_file(Dest, NConts),
+ case lists:member(preserve, Opts) of
+ true ->
+ {ok, FileInfo} = file:read_file_info(Src),
+ file:write_file_info(Dest, FileInfo);
+ false ->
+ ok
+ end.
+
+subst(Str, [{Var,Val}|Vars]) ->
+ subst(re:replace(Str,"%"++Var++"%",Val,[{return,list}]),Vars);
+subst(Str, []) ->
+ Str.
+
+%%% Start a node by executing the given start command. This node will
+%%% be used for upgrade.
+start_node(Start,ExpVsn,ExpAppsVsns) ->
+ Port = open_port({spawn_executable, Start}, []),
+ unlink(Port),
+ erlang:port_close(Port),
+ wait_node_up(permanent,ExpVsn,ExpAppsVsns).
+
+wait_node_up(ExpStatus,ExpVsn,ExpAppsVsns) ->
+ Node = node_name(?testnode),
+ wait_node_up(Node,ExpStatus,ExpVsn,lists:keysort(1,ExpAppsVsns),60).
+
+wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,0) ->
+ test_server:fail({node_not_started,app_check_failed,ExpVsn,ExpAppsVsns,
+ rpc:call(Node,release_handler,which_releases,[ExpStatus]),
+ rpc:call(Node,application,which_applications,[])});
+wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N) ->
+ case {rpc:call(Node,release_handler,which_releases,[ExpStatus]),
+ rpc:call(Node, application, which_applications, [])} of
+ {[{_,ExpVsn,_,_}],Apps} when is_list(Apps) ->
+ case [{A,V} || {A,_,V} <- lists:keysort(1,Apps),
+ lists:keymember(A,1,ExpAppsVsns)] of
+ ExpAppsVsns ->
+ {ok,Node};
+ _ ->
+ timer:sleep(2000),
+ wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
+ end;
+ _ ->
+ timer:sleep(2000),
+ wait_node_up(Node,ExpStatus,ExpVsn,ExpAppsVsns,N-1)
+ end.
+
+node_name(Sname) ->
+ {ok,Host} = inet:gethostname(),
+ list_to_atom(atom_to_list(Sname) ++ "@" ++ Host).
+
+rm_rf(Dir) ->
+ case file:read_file_info(Dir) of
+ {ok, #file_info{type = directory}} ->
+ {ok, Content} = file:list_dir_all(Dir),
+ [rm_rf(filename:join(Dir,C)) || C <- Content],
+ ok=file:del_dir(Dir),
+ ok;
+ {ok, #file_info{}} ->
+ ok=file:delete(Dir);
+ _ ->
+ ok
+ end.