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

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

-compile(export_all).

all() -> [{group,async_threads},
	  {group,no_async_threads}].

groups() ->
    [{async_threads,[],tcs()},
     {no_async_threads,[],tcs()}].

tcs() ->
    [t_sendfile_small
     ,t_sendfile_big_all
     ,t_sendfile_big_size
     ,t_sendfile_many_small
     ,t_sendfile_partial
     ,t_sendfile_offset
     ,t_sendfile_sendafter
     ,t_sendfile_recvafter
     ,t_sendfile_recvafter_remoteclose
     ,t_sendfile_sendduring
     ,t_sendfile_recvduring
     ,t_sendfile_closeduring
     ,t_sendfile_crashduring
    ].

init_per_suite(Config) ->
    case {os:type(),os:version()} of
	{{unix,sunos}, {5,8,_}} ->
	    {skip, "Solaris 8 not supported for now"};
	_ ->
	    Priv = proplists:get_value(priv_dir, Config),
	    SFilename = filename:join(Priv, "sendfile_small.html"),
	    {ok, DS} = file:open(SFilename,[write,raw]),
	    file:write(DS,"yo baby yo"),
	    file:sync(DS),
	    file:close(DS),
	    BFilename = filename:join(Priv, "sendfile_big.html"),
	    {ok, DB} = file:open(BFilename,[write,raw]),
	    [file:write(DB,[<<0:(10*8*1024*1024)>>]) || _I <- lists:seq(1,51)],
	    file:sync(DB),
	    file:close(DB),
	    [{small_file, SFilename},
	     {file_opts,[raw,binary]},
	     {big_file, BFilename}|Config]
    end.

end_per_suite(Config) ->
    file:delete(proplists:get_value(big_file, Config)).

init_per_group(async_threads,Config) ->
    case erlang:system_info(thread_pool_size) of
	0 ->
	    {skip,"No async threads"};
	_ ->
	    [{sendfile_opts,[{use_threads,true}]}|Config]
    end;
init_per_group(no_async_threads,Config) ->
    [{sendfile_opts,[{use_threads,false}]}|Config].

end_per_group(_,_Config) ->
    ok.

init_per_testcase(TC,Config) when TC == t_sendfile_recvduring;
				  TC == t_sendfile_sendduring ->
    Filename = proplists:get_value(small_file, Config),

    Send = fun(Sock) ->
		   {_Size, Data} = sendfile_file_info(Filename),
		   {ok,D} = file:open(Filename, [raw,binary,read]),
		   prim_file:sendfile(D, Sock, 0, 0, 0,
				      [],[],[]),
		   Data
	   end,

    %% Check if sendfile is supported on this platform
    case catch sendfile_send(Send) of
	ok ->
	    Config;
	Error ->
	    ct:log("Error: ~p",[Error]),
	    {skip,"Not supported"}
    end;
init_per_testcase(_Tc,Config) ->
    Config.


t_sendfile_small(Config) when is_list(Config) ->
    Filename = proplists:get_value(small_file, Config),

    Send = fun(Sock) ->
		   {Size, Data} = sendfile_file_info(Filename),
		   %% Here we make sure to test the sendfile/2 api
		   {ok, Size} = file:sendfile(Filename, Sock),
		   Data
	   end,

    ok = sendfile_send(Send).

t_sendfile_many_small(Config) when is_list(Config) ->
    Filename = proplists:get_value(small_file, Config),
    FileOpts = proplists:get_value(file_opts, Config, []),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    error_logger:add_report_handler(?MODULE,[self()]),

    Send = fun(Sock) ->
		   {Size,_} = sendfile_file_info(Filename),
		   N = 10000,
		   {ok,D} = file:open(Filename,[read|FileOpts]),
		   [begin
			{ok,Size} = file:sendfile(D,Sock,0,0,SendfileOpts)
		    end || _I <- lists:seq(1,N)],
		   file:close(D),
		   Size*N
	   end,

    ok = sendfile_send({127,0,0,1}, Send, 0),

    receive
	{stolen,Reason} ->
	    exit(Reason)
    after 200 ->
	    ok
    end.


t_sendfile_big_all(Config) when is_list(Config) ->
    Filename = proplists:get_value(big_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {ok, #file_info{size = Size}} =
		       file:read_file_info(Filename),
		   {ok, Size} = sendfile(Filename, Sock, SendfileOpts),
		   Size
	   end,

    ok = sendfile_send({127,0,0,1}, Send, 0).

t_sendfile_big_size(Config) ->
    Filename = proplists:get_value(big_file, Config),
    FileOpts = proplists:get_value(file_opts, Config, []),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    SendAll = fun(Sock) ->
		      {ok, #file_info{size = Size}} =
			  file:read_file_info(Filename),
		      {ok,D} = file:open(Filename,[read|FileOpts]),
		      {ok, Size} = file:sendfile(D, Sock,0,Size,SendfileOpts),
		      Size
	      end,

    ok = sendfile_send({127,0,0,1}, SendAll, 0).

t_sendfile_partial(Config) ->
    Filename = proplists:get_value(small_file, Config),
    FileOpts = proplists:get_value(file_opts, Config, []),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    SendSingle = fun(Sock) ->
			 {_Size, <<Data:5/binary,_/binary>>} =
			     sendfile_file_info(Filename),
			 {ok,D} = file:open(Filename,[read|FileOpts]),
			 {ok,5} = file:sendfile(D,Sock,0,5,SendfileOpts),
			 file:close(D),
			 Data
		 end,
    ok = sendfile_send(SendSingle),

    {_Size, <<FData:5/binary,SData:3/binary,_/binary>>} =
	sendfile_file_info(Filename),
    {ok,D} = file:open(Filename,[read|FileOpts]),
    {ok, <<FData/binary>>} = file:read(D,5),
    FSend = fun(Sock) ->
		    {ok,5} = file:sendfile(D,Sock,0,5,SendfileOpts),
		    FData
	    end,

    ok = sendfile_send(FSend),

    SSend = fun(Sock) ->
		    {ok,3} = file:sendfile(D,Sock,5,3,SendfileOpts),
		    SData
	    end,

    ok = sendfile_send(SSend),

    {ok, <<SData/binary>>} = file:read(D,3),

    file:close(D).

t_sendfile_offset(Config) ->
    Filename = proplists:get_value(small_file, Config),
    FileOpts = proplists:get_value(file_opts, Config, []),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {_Size, <<_:5/binary,Data:3/binary,_/binary>> = AllData} =
		       sendfile_file_info(Filename),
		   {ok,D} = file:open(Filename,[read|FileOpts]),
		   {ok,3} = file:sendfile(D,Sock,5,3,SendfileOpts),
		   {ok, AllData} = file:read(D,100),
		   file:close(D),
		   Data
	   end,
    ok = sendfile_send(Send).


t_sendfile_sendafter(Config) ->
    Filename = proplists:get_value(small_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {Size, Data} = sendfile_file_info(Filename),
		   {ok, Size} = sendfile(Filename, Sock, SendfileOpts),
		   ok = gen_tcp:send(Sock, <<2>>),
		   <<Data/binary,2>>
	   end,

    ok = sendfile_send(Send).

t_sendfile_recvafter(Config) ->
    Filename = proplists:get_value(small_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {Size, Data} = sendfile_file_info(Filename),
		   {ok, Size} = sendfile(Filename, Sock, SendfileOpts),
		   ok = gen_tcp:send(Sock, <<1>>),
		   {ok,<<1>>} = gen_tcp:recv(Sock, 1),
		   <<Data/binary,1>>
	   end,

    ok = sendfile_send(Send).

%% This tests specifically for a bug fixed in 17.0
t_sendfile_recvafter_remoteclose(Config) ->
    Filename = proplists:get_value(small_file, Config),

    Send = fun(Sock, SFServer) ->
		   {Size, _Data} = sendfile_file_info(Filename),
		   {ok, Size} = file:sendfile(Filename, Sock),

		   %% Make sure the remote end has been closed
		   SFServer ! stop,
		   timer:sleep(100),

		   %% In the bug this returned {error,ebadf}
		   {error,closed} = gen_tcp:recv(Sock, 1),
		   -1
	   end,

    ok = sendfile_send({127,0,0,1},Send,0).

t_sendfile_sendduring(Config) ->
    Filename = proplists:get_value(big_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {ok, #file_info{size = Size}} =
		       file:read_file_info(Filename),
		   spawn_link(fun() ->
				      timer:sleep(50),
				      ok = gen_tcp:send(Sock, <<2>>)
			      end),
		   {ok, Size} = sendfile(Filename, Sock, SendfileOpts),
		   Size+1
	   end,

    ok = sendfile_send({127,0,0,1}, Send, 0).

t_sendfile_recvduring(Config) ->
    Filename = proplists:get_value(big_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock) ->
		   {ok, #file_info{size = Size}} =
		       file:read_file_info(Filename),
		   spawn_link(fun() ->
				      timer:sleep(50),
				      ok = gen_tcp:send(Sock, <<1>>),
				      {ok,<<1>>} = gen_tcp:recv(Sock, 1)
			      end),
		   {ok, Size} = sendfile(Filename, Sock, SendfileOpts),
		   timer:sleep(1000),
		   Size+1
	   end,

    ok = sendfile_send({127,0,0,1}, Send, 0).

t_sendfile_closeduring(Config) ->
    Filename = proplists:get_value(big_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    Send = fun(Sock,SFServPid) ->
		   spawn_link(fun() ->
				      timer:sleep(50),
				      SFServPid ! stop
			      end),
		   case erlang:system_info(thread_pool_size) of
		       0 ->
			   {error, closed} = sendfile(Filename, Sock,
						      SendfileOpts);
		       _Else ->
			   %% This can return how much has been sent or
			   %% {error,closed} depending on OS.
			   %% How much is sent impossible to know as
			   %%  the socket was closed mid sendfile
			   case sendfile(Filename, Sock, SendfileOpts) of
			       {error, closed} ->
				   ok;
			       {ok, Size}  when is_integer(Size) ->
				   ok
			   end
		   end,
		   -1
	   end,

    ok = sendfile_send({127,0,0,1}, Send, 0).

t_sendfile_crashduring(Config) ->
    Filename = proplists:get_value(big_file, Config),
    SendfileOpts = proplists:get_value(sendfile_opts, Config),

    error_logger:add_report_handler(?MODULE,[self()]),

    Send = fun(Sock) ->
		   spawn_link(fun() ->
				      timer:sleep(50),
				      exit(die)
			      end),
		   {error, closed} = sendfile(Filename, Sock, SendfileOpts),
		   -1
	   end,
    process_flag(trap_exit,true),
    spawn_link(fun() ->
		       ok = sendfile_send({127,0,0,1}, Send, 0)
	       end),
    receive
	{stolen,Reason} ->
	    process_flag(trap_exit,false),
	    ct:fail(Reason)
	after 200 ->
		receive
		    {'EXIT',_,Reason} ->
			process_flag(trap_exit,false),
			die = Reason
		end
	end.

%% Generic sendfile server code
sendfile_send(Send) ->
    sendfile_send({127,0,0,1},Send).
sendfile_send(Host, Send) ->
    sendfile_send(Host, Send, []).
sendfile_send(Host, Send, Orig) ->
    SFServer = spawn_link(?MODULE, sendfile_server, [self(), Orig]),
    receive
	{server, Port} ->
	    {ok, Sock} = gen_tcp:connect(Host, Port,
					       [binary,{packet,0},
						{active,false}]),
	    Data = case proplists:get_value(arity,erlang:fun_info(Send)) of
		       1 ->
			   Send(Sock);
		       2 ->
			   Send(Sock, SFServer)
		   end,
	    ok = gen_tcp:close(Sock),
	    receive
		{ok, Bin} ->
		    Data = Bin,
		    ok
	    end
    end.

sendfile_server(ClientPid, Orig) ->
    {ok, LSock} = gen_tcp:listen(0, [binary, {packet, 0},
				     {active, true},
				     {reuseaddr, true}]),
    {ok, Port} = inet:port(LSock),
    ClientPid ! {server, Port},
    {ok, Sock} = gen_tcp:accept(LSock),
    {ok, Bin} = sendfile_do_recv(Sock, Orig),
    ClientPid ! {ok, Bin},
    gen_tcp:send(Sock, <<1>>).

-define(SENDFILE_TIMEOUT, 10000).
sendfile_do_recv(Sock, Bs) ->
    TimeoutMul = case os:type() of
		     {win32, _} -> 6;
		     _ -> 1
		 end,
    receive
	stop when Bs /= 0,is_integer(Bs) ->
	    gen_tcp:close(Sock),
	    {ok, -1};
	{tcp, Sock, B} ->
	    case binary:match(B,<<1>>) of
		nomatch when is_list(Bs) ->
		    sendfile_do_recv(Sock, [B|Bs]);
		nomatch when is_integer(Bs) ->
		    sendfile_do_recv(Sock, byte_size(B) + Bs);
		_ when is_list(Bs) ->
		    ct:log("Stopped due to a 1"),
		    {ok, iolist_to_binary(lists:reverse([B|Bs]))};
		_ when is_integer(Bs) ->
		    ct:log("Stopped due to a 1"),
		    {ok, byte_size(B) + Bs}
	    end;
	{tcp_closed, Sock} when is_list(Bs) ->
	    ct:log("Stopped due to close"),
	    {ok, iolist_to_binary(lists:reverse(Bs))};
	{tcp_closed, Sock} when is_integer(Bs) ->
	    ct:log("Stopped due to close"),
	    {ok, Bs}
    after ?SENDFILE_TIMEOUT * TimeoutMul ->
	    ct:log("Sendfile timeout"),
	    timeout
    end.

sendfile_file_info(File) ->
    {ok, #file_info{size = Size}} = file:read_file_info(File),
    {ok, Data} = file:read_file(File),
    {Size, Data}.

sendfile(Filename,Sock,Opts) ->
    case file:open(Filename, [read, raw, binary]) of
	{error, Reason} ->
	    {error, Reason};
	{ok, Fd} ->
	    Res = file:sendfile(Fd, Sock, 0, 0, Opts),
	    _ = file:close(Fd),
	    Res
    end.


%% Error handler 

init([Proc]) -> {ok,Proc}.

handle_event({error,noproc,{emulator,Format,Args}}, Proc) -> 
    Proc ! {stolen,lists:flatten(io_lib:format(Format,Args))},
    {ok,Proc};
handle_event(_, Proc) -> 
    {ok,Proc}.
