%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2012. 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%
%%
%% This module implements a process with the registered name 'test_server_io',
%% which has two main responsibilities:
%%
%% * Manage group leader processes (see the test_server_gl module)
%% for test cases. A group_leader process is obtained by calling
%% get_gl/1. Group leader processes will be kept alive as along as
%% the 'test_server_io' process is alive.
%%
%% * Handle output to the common log files (stdout, major, html,
%% unexpected_io).
%%
-module(test_server_io).
-export([start_link/0,stop/0,get_gl/1,set_fd/2,
start_transaction/0,end_transaction/0,print_buffered/1,print/3,
set_footer/1,set_job_name/1,set_gl_props/1]).
-export([init/1,handle_call/3,handle_info/2,terminate/2]).
-record(st, {fds, %Singleton fds (gb_tree)
shared_gl :: pid(), %Shared group leader
gls, %Group leaders (gb_set)
io_buffering=false, %I/O buffering
buffered, %Buffered I/O requests
html_footer, %HTML footer
job_name, %Name of current job.
gl_props, %Properties for GL.
stopping
}).
start_link() ->
case gen_server:start_link({local,?MODULE}, ?MODULE, [], []) of
{ok,Pid} ->
{ok,Pid};
Other ->
Other
end.
stop() ->
OldGL = group_leader(),
group_leader(self(), self()),
req(stop),
group_leader(OldGL, self()),
ok.
%% get_gl(Shared) -> Pid
%% Shared = boolean()
%% Pid = pid()
%%
%% Return a group leader (a process using the test_server_gl module).
%% If Shared is true, the shared group leader is returned (suitable for
%% running sequential test cases), otherwise a new group leader process
%% is spawned. Group leader processes will live until the
%% 'test_server_io' process is stopped.
get_gl(Shared) when is_boolean(Shared) ->
req({get_gl,Shared}).
%% set_fd(Tag, Fd) -> ok.
%% Tag = major | html | unexpected_io
%% Fd = a file descriptor (as returned by file:open/2)
%%
%% Associate a file descriptor with the given Tag. This
%% Tag can later be used in when calling to print/3.
set_fd(Tag, Fd) ->
req({set_fd,Tag,Fd}).
%% start_transaction()
%%
%% Subsequent calls to print/3 from the process executing start_transaction/0
%% will cause the messages to be buffered instead of printed directly.
start_transaction() ->
req({start_transaction,self()}).
%% end_transaction()
%%
%% End the transaction started by start_transaction/0. Subsequent calls to
%% print/3 will cause the message to be printed directly.
end_transaction() ->
req({end_transaction,self()}).
%% print(From, Tag, Msg)
%% From = pid()
%% Tag = stdout, or any tag that has been registered using set_fd/2
%% Msg = string or iolist
%%
%% Either print Msg to the file identified by Tag, or buffer the message
%% start_transaction/0 has been called from the process From.
%%
%% NOTE: The tags have various special meanings. For example, 'html'
%% is assumed to be a HTML file.
print(From, Tag, Msg) ->
req({print,From,Tag,Msg}).
%% print_buffered(Pid)
%% Pid = pid()
%%
%% Print all messages buffered in the *first* transaction buffered for Pid.
%% (If start_transaction/0 and end_transaction/0 has been called N times,
%% print_buffered/1 must be called N times to print all transactions.)
print_buffered(Pid) ->
req({print_buffered,Pid}).
%% set_footer(IoData)
%%
%% Set a footer for the file associated with the 'html' tag.
%% It will be used by print/3 to print a footer for the HTML file.
set_footer(Footer) ->
req({set_footer,Footer}).
%% set_job_name(Name)
%% Set a name for the currently running job. The name will be used
%% when printing to 'stdout'.
%%
set_job_name(Name) ->
req({set_job_name,Name}).
%% set_gl_props(PropList)
%% Set properties for group leader processes. When a group_leader process
%% is created, test_server_gl:set_props(PropList) will be called.
set_gl_props(PropList) ->
req({set_gl_props,PropList}).
%%% Internal functions.
init([]) ->
process_flag(trap_exit, true),
Empty = gb_trees:empty(),
{ok,Shared} = test_server_gl:start_link(),
{ok,#st{fds=Empty,shared_gl=Shared,gls=gb_sets:empty(),
io_buffering=gb_sets:empty(),
buffered=Empty,
html_footer="</body>\n</html>\n",
job_name="<name not set>",
gl_props=[]}}.
req(Req) ->
gen_server:call(?MODULE, Req, infinity).
handle_call({get_gl,false}, _From, #st{gls=Gls,gl_props=Props}=St) ->
{ok,Pid} = test_server_gl:start_link(),
test_server_gl:set_props(Pid, Props),
{reply,Pid,St#st{gls=gb_sets:insert(Pid, Gls)}};
handle_call({get_gl,true}, _From, #st{shared_gl=Shared}=St) ->
{reply,Shared,St};
handle_call({set_fd,Tag,Fd}, _From, #st{fds=Fds0}=St) ->
Fds = gb_trees:enter(Tag, Fd, Fds0),
{reply,ok,St#st{fds=Fds}};
handle_call({start_transaction,Pid}, _From, #st{io_buffering=Buffer0,
buffered=Buf0}=St) ->
Buf = case gb_trees:is_defined(Pid, Buf0) of
false -> gb_trees:insert(Pid, queue:new(), Buf0);
true -> Buf0
end,
Buffer = gb_sets:add(Pid, Buffer0),
{reply,ok,St#st{io_buffering=Buffer,buffered=Buf}};
handle_call({print,From,Tag,Str}, _From, St0) ->
St = output(From, Tag, Str, St0),
{reply,ok,St};
handle_call({end_transaction,Pid}, _From, #st{io_buffering=Buffer0,
buffered=Buffered0}=St0) ->
Q0 = gb_trees:get(Pid, Buffered0),
Q = queue:in(eot, Q0),
Buffered = gb_trees:update(Pid, Q, Buffered0),
Buffer = gb_sets:delete_any(Pid, Buffer0),
St = St0#st{io_buffering=Buffer,buffered=Buffered},
{reply,ok,St};
handle_call({print_buffered,Pid}, _From, #st{buffered=Buffered0}=St0) ->
Q0 = gb_trees:get(Pid, Buffered0),
Q = do_print_buffered(Q0, St0),
Buffered = gb_trees:update(Pid, Q, Buffered0),
St = St0#st{buffered=Buffered},
{reply,ok,St};
handle_call({set_footer,Footer}, _From, St) ->
{reply,ok,St#st{html_footer=Footer}};
handle_call({set_job_name,Name}, _From, St) ->
{reply,ok,St#st{job_name=Name}};
handle_call({set_gl_props,Props}, _From, #st{shared_gl=Shared}=St) ->
test_server_gl:set_props(Shared, Props),
{reply,ok,St#st{gl_props=Props}};
handle_call(stop, From, #st{shared_gl=SGL,gls=Gls0}=St0) ->
St = St0#st{gls=gb_sets:insert(SGL, Gls0),stopping=From},
gc(St),
%% Give the users of the surviving group leaders some
%% time to finish.
erlang:send_after(2000, self(), stop_group_leaders),
{noreply,St}.
handle_info({'EXIT',Pid,normal}, #st{gls=Gls0,stopping=From}=St) ->
Gls = gb_sets:delete_any(Pid, Gls0),
case gb_sets:is_empty(Gls) andalso stopping =/= undefined of
true ->
%% No more group leaders left.
gen_server:reply(From, ok),
{stop,normal,St#st{gls=Gls,stopping=undefined}};
false ->
%% Wait for more group leaders to finish.
{noreply,St#st{gls=Gls}}
end;
handle_info({'EXIT',_Pid,Reason}, _St) ->
exit(Reason);
handle_info(stop_group_leaders, #st{gls=Gls}=St) ->
%% Stop the remaining group leaders.
[test_server_gl:stop(GL) || GL <- gb_sets:to_list(Gls)],
erlang:send_after(2000, self(), kill_group_leaders),
{noreply,St};
handle_info(kill_group_leaders, #st{gls=Gls,stopping=From}=St) ->
[exit(GL, kill) || GL <- gb_sets:to_list(Gls)],
gen_server:reply(From, ok),
{stop,normal,St};
handle_info(Other, St) ->
io:format("Ignoring: ~p\n", [Other]),
{noreply,St}.
terminate(_, _) ->
ok.
output(From, Tag, Str, #st{io_buffering=Buffered,buffered=Buf0}=St) ->
case gb_sets:is_member(From, Buffered) of
false ->
do_output(Tag, Str, St),
St;
true ->
Q0 = gb_trees:get(From, Buf0),
Q = queue:in({Tag,Str}, Q0),
Buf = gb_trees:update(From, Q, Buf0),
St#st{buffered=Buf}
end.
do_output(stdout, Str, #st{job_name=undefined}) ->
io:put_chars(Str);
do_output(stdout, Str0, #st{job_name=Name}) ->
Str = io_lib:format("Testing ~s: ~s\n", [Name,Str0]),
io:put_chars(Str);
do_output(Tag, Str, #st{fds=Fds}=St) ->
case gb_trees:lookup(Tag, Fds) of
none ->
S = io_lib:format("\n*** ERROR: ~p, line ~p: No known '~p' log file\n",
[?MODULE,?LINE,Tag]),
do_output(stdout, [S,Str], St);
{value,Fd} ->
try
io:put_chars(Fd, Str),
case Tag of
html -> finalise_table(Fd, St);
_ -> ok
end
catch _:Error ->
S = io_lib:format("\n*** ERROR: ~p, line ~p: Error writing to "
"log file '~p': ~p\n",
[?MODULE,?LINE,Tag,Error]),
do_output(stdout, [S,Str], St)
end
end.
finalise_table(Fd, #st{html_footer=Footer}) ->
case file:position(Fd, {cur,0}) of
{ok,Pos} ->
%% We are writing to a seekable file. Finalise so
%% we get complete valid (and viewable) HTML code.
%% Then rewind to overwrite the finalising code.
io:put_chars(Fd, ["\n</table>\n",Footer]),
file:position(Fd, Pos);
{error,epipe} ->
%% The file is not seekable. We cannot erase what
%% we've already written --- so the reader will
%% have to wait until we're done.
ok
end.
do_print_buffered(Q0, St) ->
Item = queue:get(Q0),
Q = queue:drop(Q0),
case Item of
eot ->
Q;
{Tag,Str} ->
do_output(Tag, Str, St),
do_print_buffered(Q, St)
end.
gc(#st{gls=Gls0}) ->
InUse0 = [begin
case process_info(P, group_leader) of
{group_leader,GL} -> GL;
undefined -> undefined
end
end || P <- processes()],
InUse = ordsets:from_list(InUse0),
Gls = gb_sets:to_list(Gls0),
NotUsed = ordsets:subtract(Gls, InUse),
[test_server_gl:stop(Pid) || Pid <- NotUsed],
ok.