%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-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(emem_SUITE).

-export([all/0, suite/0,
	 init_per_testcase/2, end_per_testcase/2,
         init_per_suite/1, end_per_suite/1,
	 receive_and_save_trace/2, send_trace/2]).

-export([live_node/1,
	 'sparc_sunos5.8_32b_emt2.0'/1,
	 'pc_win2000_32b_emt2.0'/1,
	 'pc.smp_linux2.2.19pre17_32b_emt2.0'/1,
	 'powerpc_darwin7.7.0_32b_emt2.0'/1,
	 'alpha_osf1v5.1_64b_emt2.0'/1,
	 'sparc_sunos5.8_64b_emt2.0'/1,
	 'sparc_sunos5.8_32b_emt1.0'/1,
	 'pc_win2000_32b_emt1.0'/1,
	 'powerpc_darwin7.7.0_32b_emt1.0'/1,
	 'alpha_osf1v5.1_64b_emt1.0'/1,
	 'sparc_sunos5.8_64b_emt1.0'/1]).

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

-define(EMEM_64_32_COMMENT,
	"64 bit trace; this build of emem can only handle 32 bit traces").

-record(emem_res, {nodename,
		   hostname,
		   pid,
		   start_time,
		   trace_version,
		   max_word_size,
		   word_size,
		   last_values,
		   maximum,
		   exit_code}).

%%
%% Exported suite functions
%%

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap,{minutes,5}}].

all() -> 
    case test_server:is_debug() of
        true -> {skip, "Not run when debug compiled"};
        false -> test_cases()
    end.


test_cases() -> 
    [live_node, 'sparc_sunos5.8_32b_emt2.0',
     'pc_win2000_32b_emt2.0',
     'pc.smp_linux2.2.19pre17_32b_emt2.0',
     'powerpc_darwin7.7.0_32b_emt2.0',
     'alpha_osf1v5.1_64b_emt2.0',
     'sparc_sunos5.8_64b_emt2.0',
     'sparc_sunos5.8_32b_emt1.0', 'pc_win2000_32b_emt1.0',
     'powerpc_darwin7.7.0_32b_emt1.0',
     'alpha_osf1v5.1_64b_emt1.0',
     'sparc_sunos5.8_64b_emt1.0'].

init_per_testcase(Case, Config) when is_list(Config) ->
    case maybe_skip(Config) of
	{skip, _}=Skip ->
            Skip;
	ok ->
	    %% Until emem is completely stable we run these tests in a working
	    %% directory with an ignore_core_files file which will make the
	    %% search for core files ignore cores generated by this suite.
	    ignore_cores:setup(?MODULE, Case, [{testcase, Case}|Config])
    end.

end_per_testcase(_Case, Config) when is_list(Config) ->
    ignore_cores:restore(Config),
    ok.

maybe_skip(Config) ->
    DataDir = proplists:get_value(data_dir, Config),
    case filelib:is_dir(DataDir) of
	false ->
	    {skip, "No data directory"};
	true ->
	    case proplists:get_value(emem, Config) of
		undefined ->
		    {skip, "emem not found"};
		_ ->
		    ok
	    end
    end.

init_per_suite(Config) when is_list(Config) ->
    BinDir = filename:join([code:lib_dir(tools), "bin"]),
    Target = erlang:system_info(system_architecture),
    Res = (catch begin 
		     case check_dir(filename:join([BinDir, Target])) of
			 not_found -> ok;
			 TDir ->
			     check_emem(TDir, purecov),
			     check_emem(TDir, purify),
			     check_emem(TDir, debug),
			     check_emem(TDir, opt)
		     end,
		     check_emem(BinDir, opt),
		     ""
		 end),
    Res ++ ignore_cores:init(Config).

end_per_suite(Config) when is_list(Config) ->
    Config1 = lists:keydelete(emem, 1, Config),
    Config2 = lists:keydelete(emem_comment, 1, Config1),
    ignore_cores:fini(Config2).

%%
%%
%% Test cases
%%
%%

live_node(Config) when is_list(Config) ->
    {ok, EmuFlag, Port} = start_emem(Config),
    Nodename = mk_nodename(Config),
    {ok, Node} = start_node(Nodename, EmuFlag),
    NP = spawn(Node,
		     fun () ->
			     receive go -> ok end,
			     I = spawn(fun () -> ignorer end),
			     GC = fun () ->
					  GCP = fun (P) ->
							garbage_collect(P)
						end,
					  lists:foreach(GCP, processes())
				  end,
			     Seq = fun () -> I ! lists:seq(1, 1000000) end,
			     spawn_link(Seq),
			     B1 = <<0:10000000>>,
			     spawn_link(Seq),
			     B2 = <<0:10000000>>,
			     spawn_link(Seq),
			     B3 = <<0:10000000>>,
			     I ! {B1, B2, B3},
			     GC(),
			     GC(),
			     GC()
		     end),
    MRef = erlang:monitor(process, NP),
    NP ! go,
    receive
	      {'DOWN', MRef, process, NP, Reason} ->
		  spawn(Node, fun () -> halt(17) end),
		  normal = Reason
	  end,
    Res = get_emem_result(Port),
    {ok, Hostname} = inet:gethostname(),
    ShortHostname = short_hostname(Hostname),
    {true, _} = has_prefix(Nodename, Res#emem_res.nodename),
    ShortHostname = short_hostname(Res#emem_res.hostname),
    Bits = case erlang:system_info(wordsize) of
		     4 -> "32 bits";
		     8 -> "64 bits"
		 end,
    Bits = Res#emem_res.word_size,
    "17" = Res#emem_res.exit_code,
    emem_comment(Config).

'sparc_sunos5.8_32b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "gorbag" = Res#emem_res.hostname,
    "17074" = Res#emem_res.pid,
    "2005-01-14 17:28:37.881980" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["15", "2665739", "8992", "548986", "16131", "539994",
     "4334192", "1", "99", "15", "98",
     "0", "0", "49", "0", "49"] = Res#emem_res.last_values,
    ["5972061", "9662", "7987824", "5",
     "2375680", "3"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).

'pc_win2000_32b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "E-788FCF5191B54" = Res#emem_res.hostname,
    "504" = Res#emem_res.pid,
    "2005-01-24 17:27:28.224000" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["11", "2932575", "8615", "641087", "68924", "632472"]
	= Res#emem_res.last_values, 
    ["5434206", "9285"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).

'pc.smp_linux2.2.19pre17_32b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "four-roses" = Res#emem_res.hostname,
    "20689" = Res#emem_res.pid,
    "2005-01-20 13:11:26.143077" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["49", "2901817", "9011", "521610", "10875", "512599",
     "5392096", "2", "120", "10", "118",
     "0", "0", "59", "0", "59"] = Res#emem_res.last_values,
    ["6182918", "9681",
     "9062112", "6",
     "2322432", "3"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).


'powerpc_darwin7.7.0_32b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "grima" = Res#emem_res.hostname,
    "13021" = Res#emem_res.pid,
    "2005-01-20 15:08:17.568668" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["9", "2784323", "8641", "531105", "15893", "522464"]
	= Res#emem_res.last_values,
    ["6150376", "9311"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).

'alpha_osf1v5.1_64b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "thorin" = Res#emem_res.hostname,
    "224630" = Res#emem_res.pid,
    "2005-01-20 22:38:01.299632" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "64 bits" = Res#emem_res.word_size,
    case Res#emem_res.max_word_size of
        "32 bits" ->
            emem_comment(Config, ?EMEM_64_32_COMMENT);
        "64 bits" ->
            ["22",
             "6591992", "8625", "516785", "14805", "508160",
             "11429184", "5", "127", "254", "122",
             "0", "0", "61", "0", "61"] = Res#emem_res.last_values,
            ["7041775", "9295",
             "11593024", "7",
             "2097152", "3"] = Res#emem_res.maximum,
            "0" = Res#emem_res.exit_code,
            emem_comment(Config)
    end.

'sparc_sunos5.8_64b_emt2.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "test_server" = Res#emem_res.nodename,
    "gorbag" = Res#emem_res.hostname,
    "10907" = Res#emem_res.pid,
    "2005-01-20 13:48:34.677068" = Res#emem_res.start_time,
    "2.0" = Res#emem_res.trace_version,
    "64 bits" = Res#emem_res.word_size,
    case Res#emem_res.max_word_size of
        "32 bits" ->
            emem_comment(Config, ?EMEM_64_32_COMMENT);
        "64 bits" ->
            ["16",
             "5032887", "8657", "530635", "14316", "521978",
             "8627140", "5", "139", "19", "134",
             "0", "0", "67", "0", "67"] = Res#emem_res.last_values,
            ["11695070", "9324",
             "16360388", "10",
             "4136960", "3"] = Res#emem_res.maximum,
            "0" = Res#emem_res.exit_code,
            emem_comment(Config)
    end.

'sparc_sunos5.8_32b_emt1.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "" = Res#emem_res.nodename,
    "" = Res#emem_res.hostname,
    "" = Res#emem_res.pid,
    "" = Res#emem_res.start_time,
    "1.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["11", "2558261", "8643", "560610", "15325", "551967"]
	= Res#emem_res.last_values,
    ["2791121", "9317"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).

'pc_win2000_32b_emt1.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "" = Res#emem_res.nodename,
    "" = Res#emem_res.hostname,
    "" = Res#emem_res.pid,
    "" = Res#emem_res.start_time,
    "1.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["6", "2965248", "8614", "640897", "68903", "632283"]
	= Res#emem_res.last_values,
    ["3147090", "9283"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).


'powerpc_darwin7.7.0_32b_emt1.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "" = Res#emem_res.nodename,
    "" = Res#emem_res.hostname,
    "" = Res#emem_res.pid,
    "" = Res#emem_res.start_time,
    "1.0" = Res#emem_res.trace_version,
    "32 bits" = Res#emem_res.word_size,
    ["8", "2852991", "8608", "529662", "15875", "521054"]
	= Res#emem_res.last_values,
    ["3173335", "9278"] = Res#emem_res.maximum,
    "0" = Res#emem_res.exit_code,
    emem_comment(Config).

'alpha_osf1v5.1_64b_emt1.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "" = Res#emem_res.nodename,
    "" = Res#emem_res.hostname,
    "" = Res#emem_res.pid,
    "" = Res#emem_res.start_time,
    "1.0" = Res#emem_res.trace_version,
    "64 bits" = Res#emem_res.word_size,
    case Res#emem_res.max_word_size of
        "32 bits" ->
            emem_comment(Config, ?EMEM_64_32_COMMENT);
        "64 bits" ->
            ["22",
             "6820094", "8612", "515518", "14812", "506906"]
            = Res#emem_res.last_values,
            ["7292413", "9282"] = Res#emem_res.maximum,
            "0" = Res#emem_res.exit_code,
            emem_comment(Config)
    end.

'sparc_sunos5.8_64b_emt1.0'(Config) when is_list(Config) ->
    Res = run_emem_on_casefile(Config),
    "" = Res#emem_res.nodename,
    "" = Res#emem_res.hostname,
    "" = Res#emem_res.pid,
    "" = Res#emem_res.start_time,
    "1.0" = Res#emem_res.trace_version,
    "64 bits" = Res#emem_res.word_size,
    case Res#emem_res.max_word_size of
        "32 bits" ->
            emem_comment(Config, ?EMEM_64_32_COMMENT);
        "64 bits" ->
            ["15",
             "4965746", "8234", "543940", "14443", "535706"]
            = Res#emem_res.last_values,
            ["11697645", "8908"] = Res#emem_res.maximum,
            "0" = Res#emem_res.exit_code,
            emem_comment(Config)
    end.

%%
%%
%% Auxiliary functions
%%
%%

receive_and_save_trace(PortNumber, FileName) when is_integer(PortNumber),
						  is_list(FileName) ->
    {ok, F} = file:open(FileName, [write, compressed]),
    {ok, LS} = gen_tcp:listen(PortNumber, [inet, {reuseaddr,true}, binary]),
    {ok, S} = gen_tcp:accept(LS),
    gen_tcp:close(LS),
    receive_loop(S,F).

receive_loop(Socket, File) ->
    receive
        {tcp, Socket, Data} ->
            ok = file:write(File, Data),
            receive_loop(Socket, File);
        {tcp_closed, Socket} ->
            file:close(File),
            ok;
        {tcp_error, Socket, Reason} ->
            file:close(File),
            {error, Reason}
    end.

send_trace({Host, PortNumber}, FileName) when is_list(Host),
					      is_integer(PortNumber),
					      is_list(FileName) ->
    {ok, F} = file:open(FileName, [read, compressed]),
    {ok, S} = gen_tcp:connect(Host, PortNumber, [inet,{packet, 0}]),
    send_loop(S, F);
send_trace(EmuFlag, FileName) when is_list(EmuFlag),
				   is_list(FileName) ->
    ["+Mit", IpAddrStr, PortNoStr] = string:tokens(EmuFlag, " :"),
    send_trace({IpAddrStr, list_to_integer(PortNoStr)}, FileName).

send_loop(Socket, File) ->
    case file:read(File, 128) of
        {ok, Data} ->
            case gen_tcp:send(Socket, Data) of
                ok -> send_loop(Socket, File);
                Error ->
                    gen_tcp:close(Socket),
                    file:close(File),
                    Error
            end;
        eof ->
            gen_tcp:close(Socket),
            file:close(File),
            ok;
        Error ->
            gen_tcp:close(Socket),
            file:close(File),
            Error
    end.

check_emem(Dir, Type) when is_atom(Type) ->
    ExeSuffix = case os:type() of
                    {win32, _} -> ".exe";
                    _ -> ""
                end,
    TypeSuffix = case Type of
                     opt -> "";
                     _ -> "." ++ atom_to_list(Type)
                 end,
    Emem = "emem" ++ TypeSuffix ++ ExeSuffix,
    case check_file(filename:join([Dir, Emem])) of
        not_found -> ok;
        File ->
            Comment = case Type of
                          opt -> "";
                          _ -> "[emem " ++ atom_to_list(Type) ++ " compiled]"
                      end,
            throw([{emem, File}, {emem_comment, Comment}])
    end.

check_dir(DirName) ->
    case file:read_file_info(DirName) of
        {ok, #file_info {type = directory, access = A}} when A == read;
                                                             A == read_write ->
            DirName;
        _ ->
            not_found
    end.

check_file(FileName) ->
    case file:read_file_info(FileName) of
        {ok, #file_info {type = regular, access = A}} when A == read;
                                                           A == read_write ->
            FileName;
        _ ->
            not_found
    end.

emem_comment(Config) when is_list(Config) ->
    emem_comment(Config, "").

emem_comment(Config, ExtraComment)
  when is_list(Config), is_list(ExtraComment) ->
    case {proplists:get_value(emem_comment, Config), ExtraComment} of
        {"", ""} -> ok;
        {"", XC} -> {comment, XC};
        {EmemC, ""} -> {comment, EmemC};
        {EmemC, XC} -> {comment, EmemC ++ " " ++ XC}
    end.

run_emem_on_casefile(Config) ->
    CaseName = atom_to_list(proplists:get_value(testcase, Config)),
    File = filename:join([proplists:get_value(data_dir, Config), CaseName ++ ".gz"]),
    case check_file(File) of
        not_found ->
            ct:fail({error, {filenotfound, File}});
        _ ->
            ok
    end,
    {ok, EmuFlag, Port} = start_emem(Config),
    Parent = self(),
    Ref = make_ref(),
    spawn_link(fun () ->
                       SRes = send_trace(EmuFlag, File),
                       Parent ! {Ref, SRes}
               end),
    Res = get_emem_result(Port),
    receive
        {Ref, ok} ->
            ok;
        {Ref, SendError} ->
            io:format("Send result: ~p~n", [SendError])
    end,
    Res.

get_emem_result(Port) ->
    {Res, LV} = get_emem_result(Port, {#emem_res{}, []}),
    Res#emem_res{last_values = string:tokens(LV, " ")}.

get_emem_result(Port, {_EmemRes, _LastValues} = Res) ->
    case get_emem_line(Port) of
        eof ->
            Res;
        Line ->
            get_emem_result(Port, parse_emem_line(Line, Res))
    end.

parse_emem_main_header_footer_line(Line, {ER, LV} = Res) ->

    %% Header
    case has_prefix("> Nodename:", Line) of
        {true, NN} ->
            throw({ER#emem_res{nodename = strip(NN)}, LV});
        false -> ok
    end,
    case has_prefix("> Hostname:", Line) of
        {true, HN} ->
            throw({ER#emem_res{hostname = strip(HN)}, LV});
        false -> ok
    end,
    case has_prefix("> Pid:", Line) of
        {true, P} ->
            throw({ER#emem_res{pid = strip(P)}, LV});
        false -> ok
    end,
    case has_prefix("> Start time (UTC):", Line) of
        {true, ST} ->
            throw({ER#emem_res{start_time = strip(ST)}, LV});
        false -> ok
    end,
    case has_prefix("> Actual trace version:", Line) of
        {true, TV} ->
            throw({ER#emem_res{trace_version = strip(TV)}, LV});
        false -> ok
    end,
    case has_prefix("> Maximum trace word size:", Line) of
        {true, MWS} ->
            throw({ER#emem_res{max_word_size = strip(MWS)}, LV});
        false -> ok
    end,
    case has_prefix("> Actual trace word size:", Line) of
        {true, WS} ->
            throw({ER#emem_res{word_size = strip(WS)}, LV});
        false -> ok
    end,

    %% Footer
    case has_prefix("> Maximum:", Line) of
        {true, M} ->
            throw({ER#emem_res{maximum = string:tokens(M," ")}, LV});
        false -> ok
    end,
    case has_prefix("> Emulator exited with code:", Line) of
        {true, EC} ->
            throw({ER#emem_res{exit_code = strip(EC)}, LV});
        false -> ok
    end,
    Res.

parse_emem_header_line(_Line, {_ER, _LV} = Res) ->
    Res.

parse_emem_value_line(Line, {EmemRes, _OldLastValues}) ->
    {EmemRes, Line}.

parse_emem_line("", Res) ->
    Res;
parse_emem_line(Line, Res) ->
    [Prefix | _] = Line,
    case Prefix of
        $> -> catch parse_emem_main_header_footer_line(Line, Res);
        $| -> catch parse_emem_header_line(Line, Res);
        _ -> catch parse_emem_value_line(Line, Res)
    end.

start_emem(Config) when is_list(Config) ->
    Emem = proplists:get_value(emem, Config),
    Cd = case ignore_cores:dir(Config) of
             false -> [];
             Dir -> [{cd, Dir}]
         end,
    case open_port({spawn, Emem ++ " -t -n -o -i 1"},
                   Cd ++ [{line, 1024}, eof]) of
        Port when is_port(Port) -> {ok, read_emu_flag(Port), Port};
        Error -> ct:fail(Error)
    end.

read_emu_flag(Port) ->
    Line = case get_emem_line(Port) of
               eof -> ct:fail(unexpected_end_of_file);
               L -> L
           end,
    case has_prefix("> Emulator command line argument:", Line) of
        {true, EmuFlag} -> EmuFlag;
        false -> read_emu_flag(Port)
    end.

get_emem_line(Port, Acc) ->
    receive
        {Port, {data, {eol, Data}}} ->
            Res = case Acc of
                      [] -> Data;
                      _ -> lists:flatten([Acc|Data])
                  end,
            io:format("~s", [Res]),
            Res;
        {Port, {data, {noeol, Data}}} ->
            get_emem_line(Port, [Acc|Data]);
        {Port, eof} ->
            port_close(Port),
            eof
    end.

get_emem_line(Port) ->
    get_emem_line(Port, []).

short_hostname([]) ->
    [];
short_hostname([$.|_]) ->
    [];
short_hostname([C|Cs]) ->
    [C | short_hostname(Cs)].

has_prefix([], List) when is_list(List) ->
    {true, List};
has_prefix([P|Xs], [P|Ys]) ->
    has_prefix(Xs, Ys);
has_prefix(_, _) ->
    false.

strip(Str) -> string:strip(Str).

mk_nodename(Config) ->
    Us = erlang:monotonic_time(),
    atom_to_list(?MODULE)
    ++ "-" ++ atom_to_list(proplists:get_value(testcase, Config))
    ++ integer_to_list(Us).

start_node(Name, Args) ->
    Pa = filename:dirname(code:which(?MODULE)),
    test_server:start_node(Name, peer, [{args, Args ++ " -pa " ++ Pa}]).
