%%
%% %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%
-module(upgrade_SUITE).

-compile(export_all).

-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").

-define(upgr_sname,otp_upgrade).

%% Applications that are excluded from this test because they can not
%% just be started in a new node with out specific configuration.
-define(start_exclude,
	[cosEvent,cosEventDomain,cosFileTransfer,cosNotification,
	 cosProperty,cosTime,cosTransactions,erts,ic,netconf,orber,
	 safe]).

%% Applications that are excluded from this test because their appup
%% file don't support the upgrade.
%% In specific:
%% - hipe does not support any upgrade at all
%% - dialyzer requires hipe (in the .app file)
%% - typer requires hipe (in the .app file)
-define(appup_exclude, 
	[dialyzer,hipe,typer]).

init_per_suite(Config) ->
    %% 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
	    {skip, "Need a real release running to create other releases"};
	_ ->
	    rm_rf(filename:join([?config(data_dir,Config),priv_dir])),
	    Config
    end.

init_per_testcase(Case,Config) ->
    PrivDir = filename:join([?config(data_dir,Config),priv_dir,Case]),
    CreateDir = filename:join([PrivDir,create]),
    InstallDir = filename:join([PrivDir,install]),
    ok = filelib:ensure_dir(filename:join(CreateDir,"*")),
    ok = filelib:ensure_dir(filename:join(InstallDir,"*")),
    Config1 = lists:keyreplace(priv_dir,1,Config,{priv_dir,PrivDir}),
    [{create_dir,CreateDir},{install_dir,InstallDir}|Config1].

end_per_testcase(_Case,Config) ->
    Nodes = nodes(),
    [test_server:stop_node(Node) || Node <- Nodes],
    case ?config(tc_status,Config) of
	ok ->
	    %% Note that priv_dir here is per test case!
	    rm_rf(?config(priv_dir,Config));
	_fail ->
	    %% Test case data can be found under DataDir/priv_dir/Case
	    ok
    end,
    ok.

all() ->
    [minor,major].

%% If this is major release X, then this test performs an upgrade from
%% major release X-1 to the current release.
major(Config) ->
    Current = erlang:system_info(otp_release),
    PreviousMajor = previous_major(Current),
    upgrade_test(PreviousMajor,Current,Config).

%% If this is a patched version of major release X, then this test
%% performs an upgrade from major release X to the current release.
minor(Config) ->
    CurrentMajor = erlang:system_info(otp_release),
    Current = CurrentMajor++"_patched",
    upgrade_test(CurrentMajor,Current,Config).

%%%-----------------------------------------------------------------
upgrade_test(FromVsn,ToVsn,Config) ->
    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 ->
	    %% Note that priv_dir here is per test case!
	    rm_rf(?config(priv_dir,Config)),
	    {skip, "no previous release available"};
	_ ->
	    upgrade_test1(FromVsn,ToVsn,[{old_rel,OldRel}|Config])
    end.

upgrade_test1(FromVsn,ToVsn,Config) ->
    CreateDir = ?config(create_dir,Config),
    InstallDir = ?config(install_dir,Config),
    FromRelName = "otp-"++FromVsn,
    ToRelName = "otp-"++ToVsn,

    {FromRel,FromApps} = target_system(FromRelName, FromVsn,
				       CreateDir, InstallDir,Config),
    {ToRel,ToApps} = upgrade_system(FromRel, ToRelName, ToVsn,
				    CreateDir, InstallDir),
    do_upgrade(FromVsn, FromApps, ToRel, ToApps, InstallDir).

%%%-----------------------------------------------------------------
%%% 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(RelName0,RelVsn,CreateDir,InstallDir,Config) ->
    {ok,Node} = test_server:start_node(list_to_atom(RelName0),peer,
				       [{erl,[?config(old_rel,Config)]}]),

    {RelName,Apps,ErtsVsn} = create_relfile(Node,CreateDir,RelName0,RelVsn),
 
    %% Create .script and .boot
    ok = rpc:call(Node,systools,make_script,[RelName]),
    
    %% Create base tar file - i.e. erts and all apps
    ok = rpc:call(Node,systools,make_tar,
		  [RelName,[{erts,rpc:call(Node,code,root_dir,[])}]]),
    
    %% 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 and sys.config
    StartErlData = filename:join([InstallDir, "releases", "start_erl.data"]),
    write_file(StartErlData, io_lib:fwrite("~s ~s~n", [ErtsVsn, RelVsn])),
    SysConfig = filename:join([InstallDir, "releases", RelVsn, "sys.config"]),
    write_file(SysConfig, "[]."),
    
    %% Insert 'start' script from data_dir - modified to add sname and heart
    copy_file(filename:join(?config(data_dir,Config),"start.src"),
	      filename:join(ErtsBinDir,"start.src")),
    ok = file:change_mode(filename:join(ErtsBinDir,"start.src"),8#0755),

    %% Make start_erl executable
    %% (this has been fixed in OTP 17 - is 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),

    true = test_server:stop_node(Node),

    {RelName,Apps}.

%%%-----------------------------------------------------------------
%%% Create a release containing the current (the test node) OTP
%%% release, including relup to allow upgrade from an earlier OTP
%%% release.
upgrade_system(FromRel, ToRelName0, ToVsn,
	       CreateDir, InstallDir) ->

    {RelName,Apps,_} = create_relfile(node(),CreateDir,ToRelName0,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()}]),

    {RelName,Apps}.

%%%-----------------------------------------------------------------
%%% Start a new node running the release from target_system/5
%%% above. Then upgrade to the system from upgrade_system/5.
do_upgrade(FromVsn,FromApps,ToRel,ToApps,InstallDir) ->
    Start = filename:join([InstallDir,bin,start]),
    {ok,Node} = start_node(Start,permanent,FromVsn,FromApps),

    [{"OTP upgrade test",FromVsn,_,permanent}] =
	rpc:call(Node,release_handler,which_releases,[]),
    {ok,ToVsn} = rpc:call(Node,release_handler,unpack_release,[ToRel]),
    [{"OTP upgrade test",ToVsn,_,unpacked},
     {"OTP upgrade test",FromVsn,_,permanent}] =
	rpc:call(Node,release_handler,which_releases,[]),
    case rpc:call(Node,release_handler,install_release,[ToVsn]) of
	{ok,FromVsn,_} ->
	    ok;
	{continue_after_restart,FromVsn,_} ->
	    wait_node_up(current,ToVsn,ToApps)
    end,
    [{"OTP upgrade test",ToVsn,_,current},
     {"OTP upgrade test",FromVsn,_,permanent}] =
	rpc:call(Node,release_handler,which_releases,[]),
    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,[]),
    
    erlang:monitor_node(Node,true),
    _ = rpc:call(Node,init,stop,[]),
    receive {nodedown,Node} -> ok end,

    ok.

%%%-----------------------------------------------------------------
%%% Library functions
previous_major("17") ->
    "r16b";
previous_major(Rel) ->
    integer_to_list(list_to_integer(Rel)-1).

create_relfile(Node,CreateDir,RelName0,RelVsn) ->
    LibDir = rpc:call(Node,code,lib_dir,[]),
    SplitLibDir = filename:split(LibDir),
    Paths = rpc:call(Node,code,get_path,[]),
    Exclude = ?start_exclude ++ ?appup_exclude,
    Apps = lists:flatmap(
	     fun(Path) ->
		     case lists:prefix(LibDir,Path) of
			 true ->
			     case filename:split(Path) -- SplitLibDir of
				 [AppVsn,"ebin"] ->
				     case string:tokens(AppVsn,"-") of
					 [AppStr,Vsn] ->
					     App = list_to_atom(AppStr),
					     case lists:member(App,Exclude) of
						 true ->
						     [];
						 false ->
						     [{App,Vsn,restart_type(App)}]
					     end;
					 _ ->
					     []
				     end;
				 _ ->
				     []
			     end;
			 false ->
			     []
		     end
	     end,
	     Paths),

    ErtsVsn = rpc:call(Node, erlang, system_info, [version]),
    
    %% Create the .rel file
    RelContent = {release, {"OTP upgrade test", RelVsn}, {erts, ErtsVsn}, Apps},
    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,Apps,ErtsVsn}.

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).


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, Vars)
%% Vars = [{Var, Val}]
%% Var = Val = string()
%% Substitute all occurrences of %Var% for Val in Str, using the list
%% of variables in Vars.
%%
subst(Str, Vars) ->
    subst(Str, Vars, []).

subst([$%, C| Rest], Vars, Result) when $A =< C, C =< $Z ->
    subst_var([C| Rest], Vars, Result, []);
subst([$%, C| Rest], Vars, Result) when $a =< C, C =< $z ->
    subst_var([C| Rest], Vars, Result, []);
subst([$%, C| Rest], Vars, Result) when  C == $_ ->
    subst_var([C| Rest], Vars, Result, []);
subst([C| Rest], Vars, Result) ->
    subst(Rest, Vars, [C| Result]);
subst([], _Vars, Result) ->
    lists:reverse(Result).

subst_var([$%| Rest], Vars, Result, VarAcc) ->
    Key = lists:reverse(VarAcc),
    case lists:keysearch(Key, 1, Vars) of
        {value, {Key, Value}} ->
            subst(Rest, Vars, lists:reverse(Value, Result));
        false ->
            subst(Rest, Vars, [$%| VarAcc ++ [$%| Result]])
    end;
subst_var([C| Rest], Vars, Result, VarAcc) ->
    subst_var(Rest, Vars, Result, [C| VarAcc]);
subst_var([], Vars, Result, VarAcc) ->
    subst([], Vars, [VarAcc ++ [$%| Result]]).


%%%-----------------------------------------------------------------
%%% 
start_node(Start,ExpStatus,ExpVsn,ExpApps) ->
    case open_port({spawn_executable, Start}, []) of
        Port when is_port(Port) ->
            unlink(Port),
            erlang:port_close(Port),
	    wait_node_up(ExpStatus,ExpVsn,ExpApps);
        Error ->
            Error
    end.

wait_node_up(ExpStatus,ExpVsn,ExpApps0) ->
    ExpApps = [{A,V} || {A,V,_T} <- ExpApps0],
    Node = node_name(?upgr_sname),
    wait_node_up(Node,ExpStatus,ExpVsn,lists:keysort(1,ExpApps),60).

wait_node_up(Node,ExpStatus,ExpVsn,ExpApps,0) ->
    ct:fail({app_check_failed,ExpVsn,ExpApps,
	     rpc:call(Node,release_handler,which_releases,[ExpStatus]),
	     rpc:call(Node,application,which_applications,[])});
wait_node_up(Node,ExpStatus,ExpVsn,ExpApps,N) ->
    timer:sleep(2000),
    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)] of
		ExpApps -> {ok,Node};
		_ -> wait_node_up(Node,ExpStatus,ExpVsn,ExpApps,N-1)
	    end;
	_ ->
	    wait_node_up(Node,ExpStatus,ExpVsn,ExpApps,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.