/*  Part of SWI-Prolog

    Author:        Jan Wielemaker
    E-mail:        J.Wielemaker@cs.vu.nl
    WWW:           http://www.swi-prolog.org
    Copyright (C): 2006-2014, University of Amsterdam
			      VU University Amsterdam

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

    As a special exception, if you link this library with other files,
    compiled with a Free Software compiler, to produce an executable, this
    library does not by itself cause the resulting executable to be covered
    by the GNU General Public License. This exception does not however
    invalidate any other reasons why the executable file might be covered by
    the GNU General Public License.

    Alternatively, this program may be distributed under the Perl
    Artistic License, version 2.0.
*/

:- module(plunit,
	  [ set_test_options/1,		% +Options
	    begin_tests/1,		% +Name
	    begin_tests/2,		% +Name, +Options
	    end_tests/1,		% +Name
	    run_tests/0,		% Run all tests
	    run_tests/1,		% Run named test-set
	    load_test_files/1,		% +Options
	    running_tests/0,		% Prints currently running test
	    test_report/1		% +What
	  ]).

/** <module> Unit Testing

Unit testing environment for SWI-Prolog and   SICStus Prolog. For usage,
please visit http://www.swi-prolog.org/pldoc/package/plunit.html.

@author		Jan Wielemaker
@license	GPL+SWI-exception or Artistic 2.0
*/

:- use_module(library(apply)).
:- use_module(library(ordsets), [ord_intersection/3]).
:- meta_predicate valid_options(+, 1).


		 /*******************************
		 *    CONDITIONAL COMPILATION	*
		 *******************************/

:- discontiguous
	user:term_expansion/2.

:- dynamic
	include_code/1.

including :-
	include_code(X), !,
	X == true.
including.

if_expansion((:- if(G)), []) :-
	(   including
	->  (   catch(G, E, (print_message(error, E), fail))
	    ->  asserta(include_code(true))
	    ;   asserta(include_code(false))
	    )
	;   asserta(include_code(else_false))
	).
if_expansion((:- else), []) :-
	(   retract(include_code(X))
	->  (   X == true
	    ->  X2 = false
	    ;   X == false
	    ->	X2 = true
	    ;	X2 = X
	    ),
	    asserta(include_code(X2))
	;   throw_error(context_error(no_if),_)
	).
if_expansion((:- endif), []) :-
	retract(include_code(_)), !.

if_expansion(_, []) :-
	\+ including.

user:term_expansion(In, Out) :-
	prolog_load_context(module, plunit),
	if_expansion(In, Out).

swi     :- catch(current_prolog_flag(dialect, swi), _, fail), !.
swi     :- catch(current_prolog_flag(dialect, yap), _, fail).
sicstus :- catch(current_prolog_flag(system_type, _), _, fail).


:- if(swi).
throw_error(Error_term,Impldef) :-
	throw(error(Error_term,context(Impldef,_))).

:- set_prolog_flag(generate_debug_info, false).
:- use_module(library(option)).
:- use_module(library(pairs)).

current_test_flag(Name, Value) :-
	current_prolog_flag(Name, Value).

set_test_flag(Name, Value) :-
	create_prolog_flag(Name, Value, []).

% ensure expansion to avoid tracing
goal_expansion(forall(C,A),
	       \+ (C, \+ A)).
goal_expansion(current_module(Module,File),
	       module_property(Module, file(File))).

:- if(current_prolog_flag(dialect, yap)).

'$set_predicate_attribute'(_, _, _).

:- endif.
:- endif.

:- if(sicstus).
throw_error(Error_term,Impldef) :-
	throw(error(Error_term,i(Impldef))). % SICStus 3 work around

:- use_module(swi).			% SWI-Compatibility
:- use_module(library(terms)).
:- op(700, xfx, =@=).

'$set_source_module'(_, _).

%%	current_test_flag(?Name, ?Value) is nondet.
%
%	Query  flags  that  control  the    testing   process.  Emulates
%	SWI-Prologs flags.

:- dynamic test_flag/2.	% Name, Val

current_test_flag(optimise, Val) :-
	current_prolog_flag(compiling, Compiling),
	(   Compiling == debugcode ; true % TBD: Proper test
	->  Val = false
	;   Val = true
	).
current_test_flag(Name, Val) :-
	test_flag(Name, Val).


%%	set_test_flag(+Name, +Value) is det.

set_test_flag(Name, Val) :-
	var(Name), !,
	throw_error(instantiation_error, set_test_flag(Name,Val)).
set_test_flag( Name, Val ) :-
	retractall(test_flag(Name,_)),
	asserta(test_flag(Name, Val)).

:- op(1150, fx, thread_local).

user:term_expansion((:- thread_local(PI)), (:- dynamic(PI))) :-
	prolog_load_context(module, plunit).

:- endif.

		 /*******************************
		 *	      IMPORTS		*
		 *******************************/

:- use_module(library(lists)).

:- initialization
   (   current_test_flag(test_options, _)
   ->  true
   ;   set_test_flag(test_options,
		     [ run(make),	% run tests on make/0
		       sto(false)
		     ])
   ).

%%	set_test_options(+Options)
%
%	Specifies how to deal with test suites.  Defined options are:
%
%		* load(+Load)
%		Whether or not the tests must be loaded.  Values are
%		=never=, =always=, =normal= (only if not optimised)
%
%		* run(+When)
%		When the tests are run.  Values are =manual=, =make=
%		or make(all).
%
%		* silent(+Bool)
%		If =true= (default =false=), report successful tests
%		using message level =silent=, only printing errors and
%		warnings.
%
%		* sto(+Bool)
%		How to test whether code is subject to occurs check
%		(STO).  If =false= (default), STO is not considered.
%		If =true= and supported by the hosting Prolog, code
%		is run in all supported unification mode and reported
%		if the results are inconsistent.
%
%		* cleanup(+Bool)
%		If =true= (default =false), cleanup report at the end
%		of run_tests/1.  Used to improve cooperation with
%		memory debuggers such as dmalloc.

set_test_options(Options) :-
	valid_options(Options, global_test_option),
	set_test_flag(test_options, Options).

global_test_option(load(Load)) :-
	must_be(oneof([never,always,normal]), Load).
global_test_option(run(When)) :-
	must_be(oneof([manual,make,make(all)]), When).
global_test_option(silent(Bool)) :-
	must_be(boolean, Bool).
global_test_option(sto(Bool)) :-
	must_be(boolean, Bool).
global_test_option(cleanup(Bool)) :-
	must_be(boolean, Bool).


%%	loading_tests
%
%	True if tests must be loaded.

loading_tests :-
	current_test_flag(test_options, Options),
	option(load(Load), Options, normal),
	(   Load == always
	->  true
	;   Load == normal,
	    \+ current_test_flag(optimise, true)
	).

		 /*******************************
		 *	      MODULE		*
		 *******************************/

:- dynamic
	loading_unit/4,			% Unit, Module, File, OldSource
	current_unit/4,			% Unit, Module, Context, Options
	test_file_for/2.		% ?TestFile, ?PrologFile

%%	begin_tests(+UnitName:atom) is det.
%%	begin_tests(+UnitName:atom, Options) is det.
%
%	Start a test-unit. UnitName is the  name   of  the test set. the
%	unit is ended by :- end_tests(UnitName).

begin_tests(Unit) :-
	begin_tests(Unit, []).

begin_tests(Unit, Options) :-
	valid_options(Options, test_set_option),
	make_unit_module(Unit, Name),
	source_location(File, Line),
	begin_tests(Unit, Name, File:Line, Options).

:- if(swi).
begin_tests(Unit, Name, File:Line, Options) :-
	loading_tests, !,
	'$set_source_module'(Context, Context),
	(   current_unit(Unit, Name, Context, Options)
	->  true
	;   retractall(current_unit(Unit, Name, _, _)),
	    assert(current_unit(Unit, Name, Context, Options))
	),
	'$set_source_module'(Old, Name),
	'$declare_module'(Name, test, Context, File, Line, false),
	discontiguous(Name:'unit test'/4),
	'$set_predicate_attribute'(Name:'unit test'/4, trace, 0),
	discontiguous(Name:'unit body'/2),
	asserta(loading_unit(Unit, Name, File, Old)).
begin_tests(Unit, Name, File:_Line, _Options) :-
	'$set_source_module'(Old, Old),
	asserta(loading_unit(Unit, Name, File, Old)).

:- else.

% we cannot use discontiguous as a goal in SICStus Prolog.

user:term_expansion((:- begin_tests(Set)),
		    [ (:- begin_tests(Set)),
		      (:- discontiguous(test/2)),
		      (:- discontiguous('unit body'/2)),
		      (:- discontiguous('unit test'/4))
		    ]).

begin_tests(Unit, Name, File:_Line, Options) :-
	loading_tests, !,
	(   current_unit(Unit, Name, _, Options)
	->  true
	;   retractall(current_unit(Unit, Name, _, _)),
	    assert(current_unit(Unit, Name, -, Options))
	),
	asserta(loading_unit(Unit, Name, File, -)).
begin_tests(Unit, Name, File:_Line, _Options) :-
	asserta(loading_unit(Unit, Name, File, -)).

:- endif.

%%	end_tests(+Name) is det.
%
%	Close a unit-test module.
%
%	@tbd	Run tests/clean module?
%	@tbd	End of file?

end_tests(Unit) :-
	loading_unit(StartUnit, _, _, _), !,
	(   Unit == StartUnit
	->  once(retract(loading_unit(StartUnit, _, _, Old))),
	    '$set_source_module'(_, Old)
	;   throw_error(context_error(plunit_close(Unit, StartUnit)), _)
	).
end_tests(Unit) :-
	throw_error(context_error(plunit_close(Unit, -)), _).

%%	make_unit_module(+Name, -ModuleName) is det.
%%	unit_module(+Name, -ModuleName) is det.

:- if(swi).

unit_module(Unit, Module) :-
	atom_concat('plunit_', Unit, Module).

make_unit_module(Unit, Module) :-
	unit_module(Unit, Module),
	(   current_module(Module),
	    \+ current_unit(_, Module, _, _),
	    predicate_property(Module:H, _P),
	    \+ predicate_property(Module:H, imported_from(_M))
	->  throw_error(permission_error(create, plunit, Unit),
			'Existing module')
	;  true
	).

:- else.

:- dynamic
	unit_module_store/2.

unit_module(Unit, Module) :-
	unit_module_store(Unit, Module), !.

make_unit_module(Unit, Module) :-
	prolog_load_context(module, Module),
	assert(unit_module_store(Unit, Module)).

:- endif.

		 /*******************************
		 *	     EXPANSION		*
		 *******************************/

%%	expand_test(+Name, +Options, +Body, -Clause) is det.
%
%	Expand test(Name, Options) :-  Body  into   a  clause  for
%	'unit test'/4 and 'unit body'/2.

expand_test(Name, Options0, Body,
	    [ 'unit test'(Name, Line, Options, Module:'unit body'(Id, Vars)),
	      ('unit body'(Id, Vars) :- !, Body)
	    ]) :-
	source_location(_File, Line),
	prolog_load_context(module, Module),
	atomic_list_concat([Name, '@line ', Line], Id),
	term_variables(Options0, OptionVars0), sort(OptionVars0, OptionVars),
	term_variables(Body, BodyVars0), sort(BodyVars0, BodyVars),
	ord_intersection(OptionVars, BodyVars, VarList),
	Vars =.. [vars|VarList],
	(   is_list(Options0)		% allow for single option without list
	->  Options1 = Options0
	;   Options1 = [Options0]
	),
	maplist(expand_option, Options1, Options2),
	valid_options(Options2, test_option),
	valid_test_mode(Options2, Options).

expand_option(Var, _) :-
	var(Var), !,
	throw_error(instantiation_error,_).
expand_option(A == B, true(A==B)) :- !.
expand_option(A = B, true(A=B)) :- !.
expand_option(A =@= B, true(A=@=B)) :- !.
expand_option(A =:= B, true(A=:=B)) :- !.
expand_option(error(X), throws(error(X, _))) :- !.
expand_option(exception(X), throws(X)) :- !. % SICStus 4 compatibility
expand_option(error(F,C), throws(error(F,C))) :- !. % SICStus 4 compatibility
expand_option(true, true(true)) :- !.
expand_option(O, O).

valid_test_mode(Options0, Options) :-
	include(test_mode, Options0, Tests),
	(   Tests == []
	->  Options = [true(true)|Options0]
	;   Tests = [_]
	->  Options = Options0
	;   throw_error(plunit(incompatible_options, Tests), _)
	).

test_mode(true(_)).
test_mode(all(_)).
test_mode(set(_)).
test_mode(fail).
test_mode(throws(_)).


%%	expand(+Term, -Clauses) is semidet.

expand(end_of_file, _) :-
	loading_unit(Unit, _, _, _), !,
	end_tests(Unit),		% warn?
	fail.
expand((:-end_tests(_)), _) :- !,
	fail.
expand(_Term, []) :-
	\+ loading_tests.
expand((test(Name) :- Body), Clauses) :- !,
	expand_test(Name, [], Body, Clauses).
expand((test(Name, Options) :- Body), Clauses) :- !,
	expand_test(Name, Options, Body, Clauses).
expand(test(Name), _) :- !,
	throw_error(existence_error(body, test(Name)), _).
expand(test(Name, _Options), _) :- !,
	throw_error(existence_error(body, test(Name)), _).

:- if(swi).
:- multifile
	system:term_expansion/2.
:- endif.

system:term_expansion(Term, Expanded) :-
	(   loading_unit(_, _, File, _)
	->  source_location(File, _),
	    expand(Term, Expanded)
	).


		 /*******************************
		 *	       OPTIONS		*
		 *******************************/

:- if(swi).
:- use_module(library(error)).
:- else.
must_be(list, X) :- !,
	(   is_list(X)
	->  true
	;   is_not(list, X)
	).
must_be(Type, X) :-
	(   call(Type, X)
	->  true
	;   is_not(Type, X)
	).

is_not(Type, X) :-
	(   ground(X)
	->  throw_error(type_error(Type, X), _)
	;   throw_error(instantiation_error, _)
	).
:- endif.

%%	valid_options(+Options, :Pred) is det.
%
%	Verify Options to be a list of valid options according to
%	Pred.
%
%	@throws	=type_error= or =instantiation_error=.

valid_options(Options, Pred) :-
	must_be(list, Options),
	verify_options(Options, Pred).

verify_options([], _).
verify_options([H|T], Pred) :-
	(   call(Pred, H)
	->  verify_options(T, Pred)
	;   throw_error(domain_error(Pred, H), _)
	).


%%	test_option(+Option) is semidet.
%
%	True if Option is a valid option for test(Name, Options).

test_option(Option) :-
	test_set_option(Option), !.
test_option(true(_)).
test_option(fail).
test_option(throws(_)).
test_option(all(_)).
test_option(set(_)).
test_option(nondet).
test_option(fixme(_)).
test_option(forall(X)) :-
	must_be(callable, X).

%%	test_option(+Option) is semidet.
%
%	True if Option is a valid option for :- begin_tests(Name,
%	Options).

test_set_option(blocked(X)) :-
	must_be(ground, X).
test_set_option(condition(X)) :-
	must_be(callable, X).
test_set_option(setup(X)) :-
	must_be(callable, X).
test_set_option(cleanup(X)) :-
	must_be(callable, X).
test_set_option(sto(V)) :-
	nonvar(V), member(V, [finite_trees, rational_trees]).


		 /*******************************
		 *	  RUNNING TOPLEVEL	*
		 *******************************/

:- thread_local
	passed/5,			% Unit, Test, Line, Det, Time
	failed/4,			% Unit, Test, Line, Reason
	failed_assertion/7,		% Unit, Test, Line, ALoc, STO, Reason, Goal
	blocked/4,			% Unit, Test, Line, Reason
	sto/4,				% Unit, Test, Line, Results
	fixme/5.			% Unit, Test, Line, Reason, Status

:- dynamic
	running/5.			% Unit, Test, Line, STO, Thread

%%	run_tests is semidet.
%%	run_tests(+TestSet) is semidet.
%
%	Run  tests  and  report  about    the   results.  The  predicate
%	run_tests/0 runs all known  tests  that   are  not  blocked. The
%	predicate run_tests/1 takes a  specification   of  tests to run.
%	This  is  either  a  single   specification    or   a   list  of
%	specifications. Each single specification is  either the name of
%	a test-unit or a term <test-unit>:<test>, denoting a single test
%	within a unit.

run_tests :-
	cleanup,
	setup_call_cleanup(
	    setup_trap_assertions(Ref),
	    run_current_units,
	    report_and_cleanup(Ref)).

run_current_units :-
	forall(current_test_set(Set),
	       run_unit(Set)),
	check_for_test_errors.

report_and_cleanup(Ref) :-
	cleanup_trap_assertions(Ref),
	report,
	cleanup_after_test.

run_tests(Set) :-
	cleanup,
	setup_call_cleanup(
	    setup_trap_assertions(Ref),
	    run_unit_and_check_errors(Set),
	    report_and_cleanup(Ref)).

run_unit_and_check_errors(Set) :-
	run_unit(Set),
	check_for_test_errors.

run_unit([]) :- !.
run_unit([H|T]) :- !,
	run_unit(H),
	run_unit(T).
run_unit(Spec) :-
	unit_from_spec(Spec, Unit, Tests, Module, UnitOptions),
	(   option(blocked(Reason), UnitOptions)
	->  info(plunit(blocked(unit(Unit, Reason))))
	;   setup(Module, unit(Unit), UnitOptions)
	->  info(plunit(begin(Spec))),
	    forall((Module:'unit test'(Name, Line, Options, Body),
		    matching_test(Name, Tests)),
		   run_test(Unit, Name, Line, Options, Body)),
	    info(plunit(end(Spec))),
	    (	message_level(silent)
	    ->	true
	    ;	format(user_error, '~N', [])
	    ),
	    cleanup(Module, UnitOptions)
	;   true
	).

unit_from_spec(Unit, Unit, _, Module, Options) :-
	atom(Unit), !,
	(   current_unit(Unit, Module, _Supers, Options)
	->  true
	;   throw_error(existence_error(unit_test, Unit), _)
	).
unit_from_spec(Unit:Tests, Unit, Tests, Module, Options) :-
	atom(Unit), !,
	(   current_unit(Unit, Module, _Supers, Options)
	->  true
	;   throw_error(existence_error(unit_test, Unit), _)
	).


matching_test(X, X) :- !.
matching_test(Name, Set) :-
	is_list(Set),
	memberchk(Name, Set).

cleanup :-
	thread_self(Me),
	retractall(passed(_, _, _, _, _)),
	retractall(failed(_, _, _, _)),
	retractall(failed_assertion(_, _, _, _, _, _, _)),
	retractall(blocked(_, _, _, _)),
	retractall(sto(_, _, _, _)),
	retractall(fixme(_, _, _, _, _)),
	retractall(running(_,_,_,_,Me)).

cleanup_after_test :-
	current_test_flag(test_options, Options),
	option(cleanup(Cleanup), Options, false),
	(   Cleanup == true
	->  cleanup
	;   true
	).


%%	run_tests_in_files(+Files:list)	is det.
%
%	Run all test-units that appear in the given Files.

run_tests_in_files(Files) :-
	findall(Unit, unit_in_files(Files, Unit), Units),
	(   Units == []
	->  true
	;   run_tests(Units)
	).

unit_in_files(Files, Unit) :-
	is_list(Files), !,
	member(F, Files),
	absolute_file_name(F, Source,
			   [ file_type(prolog),
			     access(read),
			     file_errors(fail)
			   ]),
	unit_file(Unit, Source).


		 /*******************************
		 *	   HOOKING MAKE/0	*
		 *******************************/

%%	make_run_tests(+Files)
%
%	Called indirectly from make/0 after Files have been reloaded.

make_run_tests(Files) :-
	current_test_flag(test_options, Options),
	option(run(When), Options, manual),
	(   When == make
	->  run_tests_in_files(Files)
	;   When == make(all)
	->  run_tests
	;   true
	).

:- if(swi).

unification_capability(sto_error_incomplete).
% can detect some (almost all) STO runs
unification_capability(rational_trees).
unification_capability(finite_trees).

set_unification_capability(Cap) :-
	cap_to_flag(Cap, Flag),
	set_prolog_flag(occurs_check, Flag).

current_unification_capability(Cap) :-
	current_prolog_flag(occurs_check, Flag),
	cap_to_flag(Cap, Flag), !.

cap_to_flag(sto_error_incomplete, error).
cap_to_flag(rational_trees, false).
cap_to_flag(finite_trees, true).

:- else.
:- if(sicstus).

unification_capability(rational_trees).
set_unification_capability(rational_trees).
current_unification_capability(rational_trees).

:- else.

unification_capability(_) :-
	fail.

:- endif.
:- endif.

		 /*******************************
		 *	ASSERTION HANDLING	*
		 *******************************/

:- if(swi).

:- dynamic prolog:assertion_failed/2.

setup_trap_assertions(Ref) :-
	asserta((prolog:assertion_failed(Reason, Goal) :-
			test_assertion_failed(Reason, Goal)),
		Ref).

cleanup_trap_assertions(Ref) :-
	erase(Ref).

test_assertion_failed(Reason, Goal) :-
	thread_self(Me),
	running(Unit, Test, Line, STO, Me),
	(   catch(get_prolog_backtrace(10, Stack), _, fail),
	    assertion_location(Stack, AssertLoc)
	->  true
	;   AssertLoc = unknown
	),
	current_test_flag(test_options, Options),
	report_failed_assertion(Unit, Test, Line, AssertLoc,
				STO, Reason, Goal, Options),
	assert_cyclic(failed_assertion(Unit, Test, Line, AssertLoc,
				       STO, Reason, Goal)).

assertion_location(Stack, File:Line) :-
	append(_, [AssertFrame,CallerFrame|_], Stack),
	prolog_stack_frame_property(AssertFrame,
				    predicate(prolog_debug:assertion/1)), !,
	prolog_stack_frame_property(CallerFrame, location(File:Line)).

report_failed_assertion(Unit, Test, Line, AssertLoc,
			STO, Reason, Goal, _Options) :-
	print_message(
	    error,
	    plunit(failed_assertion(Unit, Test, Line, AssertLoc,
				    STO, Reason, Goal))).

:- else.

setup_trap_assertions(_).
cleanup_trap_assertions(_).

:- endif.





		 /*******************************
		 *	   RUNNING A TEST	*
		 *******************************/

%%	run_test(+Unit, +Name, +Line, +Options, +Body) is det.
%
%	Run a single test.

run_test(Unit, Name, Line, Options, Body) :-
	option(forall(Generator), Options), !,
	unit_module(Unit, Module),
	term_variables(Generator, Vars),
	forall(Module:Generator,
	       run_test_once(Unit, @(Name,Vars), Line, Options, Body)).
run_test(Unit, Name, Line, Options, Body) :-
	run_test_once(Unit, Name, Line, Options, Body).

run_test_once(Unit, Name, Line, Options, Body) :-
	current_test_flag(test_options, GlobalOptions),
	option(sto(false), GlobalOptions, false), !,
	current_unification_capability(Type),
	begin_test(Unit, Name, Line, Type),
	run_test_6(Unit, Name, Line, Options, Body, Result),
	end_test(Unit, Name, Line, Type),
	report_result(Result, Options).
run_test_once(Unit, Name, Line, Options, Body) :-
	current_unit(Unit, _Module, _Supers, UnitOptions),
	option(sto(Type), UnitOptions),
	\+ option(sto(_), Options), !,
	current_unification_capability(Cap0),
	call_cleanup(run_test_cap(Unit, Name, Line, [sto(Type)|Options], Body),
		     set_unification_capability(Cap0)).
run_test_once(Unit, Name, Line, Options, Body) :-
	current_unification_capability(Cap0),
	call_cleanup(run_test_cap(Unit, Name, Line, Options, Body),
		     set_unification_capability(Cap0)).

run_test_cap(Unit, Name, Line, Options, Body) :-
	(   option(sto(Type), Options)
	->  unification_capability(Type),
	    set_unification_capability(Type),
	    begin_test(Unit, Name, Line, Type),
	    run_test_6(Unit, Name, Line, Options, Body, Result),
	    end_test(Unit, Name, Line, Type),
	    report_result(Result, Options)
	;   findall(Key-(Type+Result),
		    test_caps(Type, Unit, Name, Line, Options, Body, Result, Key),
		    Pairs),
	    group_pairs_by_key(Pairs, Keyed),
	    (	Keyed == []
	    ->	true
	    ;	Keyed = [_-Results]
	    ->	Results = [_Type+Result|_],
		report_result(Result, Options)		% consistent results
	    ;	pairs_values(Pairs, ResultByType),
		report_result(sto(Unit, Name, Line, ResultByType), Options)
	    )
	).

%%	test_caps(-Type, +Unit, +Name, +Line, +Options, +Body, -Result, -Key) is nondet.

test_caps(Type, Unit, Name, Line, Options, Body, Result, Key) :-
	unification_capability(Type),
	set_unification_capability(Type),
	begin_test(Unit, Name, Line, Type),
	run_test_6(Unit, Name, Line, Options, Body, Result),
	end_test(Unit, Name, Line, Type),
	result_to_key(Result, Key),
	Key \== setup_failed.

result_to_key(blocked(_, _, _, _), blocked).
result_to_key(failure(_, _, _, How0), failure(How1)) :-
	( How0 = succeeded(_T) -> How1 = succeeded ; How0 = How1 ).
result_to_key(success(_, _, _, Determinism, _), success(Determinism)).
result_to_key(setup_failed(_,_,_), setup_failed).

report_result(blocked(Unit, Name, Line, Reason), _) :- !,
	assert(blocked(Unit, Name, Line, Reason)).
report_result(failure(Unit, Name, Line, How), Options) :- !,
	failure(Unit, Name, Line, How, Options).
report_result(success(Unit, Name, Line, Determinism, Time), Options) :- !,
	success(Unit, Name, Line, Determinism, Time, Options).
report_result(setup_failed(_Unit, _Name, _Line), _Options).
report_result(sto(Unit, Name, Line, ResultByType), Options) :-
	assert(sto(Unit, Name, Line, ResultByType)),
	print_message(error, plunit(sto(Unit, Name, Line))),
	report_sto_results(ResultByType, Options).

report_sto_results([], _).
report_sto_results([Type+Result|T], Options) :-
	print_message(error, plunit(sto(Type, Result))),
	report_sto_results(T, Options).


%%	run_test_6(+Unit, +Name, +Line, +Options, :Body, -Result) is det.
%
%	Result is one of:
%
%		* blocked(Unit, Name, Line, Reason)
%		* failure(Unit, Name, Line, How)
%		* success(Unit, Name, Line, Determinism, Time)
%		* setup_failed(Unit, Name, Line)

run_test_6(Unit, Name, Line, Options, _Body,
	   blocked(Unit, Name, Line, Reason)) :-
	option(blocked(Reason), Options), !.
run_test_6(Unit, Name, Line, Options, Body, Result) :-
	option(all(Answer), Options), !,		% all(Bindings)
	nondet_test(all(Answer), Unit, Name, Line, Options, Body, Result).
run_test_6(Unit, Name, Line, Options, Body, Result) :-
	option(set(Answer), Options), !,		% set(Bindings)
	nondet_test(set(Answer), Unit, Name, Line, Options, Body, Result).
run_test_6(Unit, Name, Line, Options, Body, Result) :-
	option(fail, Options), !,			% fail
	unit_module(Unit, Module),
	(   setup(Module, test(Unit,Name,Line), Options)
	->  statistics(runtime, [T0,_]),
	    (   catch(Module:Body, E, true)
	    ->  (   var(E)
		->  statistics(runtime, [T1,_]),
		    Time is (T1 - T0)/1000.0,
		    Result = failure(Unit, Name, Line, succeeded(Time)),
		    cleanup(Module, Options)
		;   Result = failure(Unit, Name, Line, E),
		    cleanup(Module, Options)
		)
	    ;   statistics(runtime, [T1,_]),
		Time is (T1 - T0)/1000.0,
		Result = success(Unit, Name, Line, true, Time),
		cleanup(Module, Options)
	    )
	;   Result = setup_failed(Unit, Name, Line)
	).
run_test_6(Unit, Name, Line, Options, Body, Result) :-
	option(true(Cmp), Options), !,
	unit_module(Unit, Module),
	(   setup(Module, test(Unit,Name,Line), Options) % true(Binding)
	->  statistics(runtime, [T0,_]),
	    (   catch(call_det(Module:Body, Det), E, true)
	    ->  (   var(E)
		->  statistics(runtime, [T1,_]),
		    Time is (T1 - T0)/1000.0,
		    (   catch(Module:Cmp, E, true)
		    ->  (   var(E)
			->  Result = success(Unit, Name, Line, Det, Time)
			;   Result = failure(Unit, Name, Line, cmp_error(Cmp, E))
			)
		    ;   Result = failure(Unit, Name, Line, wrong_answer(Cmp))
		    ),
		    cleanup(Module, Options)
		;   Result = failure(Unit, Name, Line, E),
		    cleanup(Module, Options)
		)
	    ;   Result = failure(Unit, Name, Line, failed),
		cleanup(Module, Options)
	    )
	;   Result = setup_failed(Unit, Name, Line)
	).
run_test_6(Unit, Name, Line, Options, Body, Result) :-
	option(throws(Expect), Options), !,
	unit_module(Unit, Module),
	(   setup(Module, test(Unit,Name,Line), Options)
	->  statistics(runtime, [T0,_]),
	    (   catch(Module:Body, E, true)
	    ->  (   var(E)
		->  Result = failure(Unit, Name, Line, no_exception),
		    cleanup(Module, Options)
		;   statistics(runtime, [T1,_]),
		    Time is (T1 - T0)/1000.0,
		    (   match_error(Expect, E)
		    ->  Result = success(Unit, Name, Line, true, Time)
		    ;   Result = failure(Unit, Name, Line, wrong_error(Expect, E))
		    ),
		    cleanup(Module, Options)
		)
	    ;   Result = failure(Unit, Name, Line, failed),
		cleanup(Module, Options)
	    )
	;   Result = setup_failed(Unit, Name, Line)
	).


%%	non_det_test(+Expected, +Unit, +Name, +Line, +Options, +Body, -Result)
%
%	Run tests on non-deterministic predicates.

nondet_test(Expected, Unit, Name, Line, Options, Body, Result) :-
	unit_module(Unit, Module),
	result_vars(Expected, Vars),
	statistics(runtime, [T0,_]),
	(   setup(Module, test(Unit,Name,Line), Options)
	->  (   catch(findall(Vars, Module:Body, Bindings), E, true)
	    ->  (   var(E)
		->  statistics(runtime, [T1,_]),
		    Time is (T1 - T0)/1000.0,
		    (   nondet_compare(Expected, Bindings, Unit, Name, Line)
		    ->  Result = success(Unit, Name, Line, true, Time)
		    ;   Result = failure(Unit, Name, Line, wrong_answer(Expected, Bindings))
		    ),
		    cleanup(Module, Options)
		;   Result = failure(Unit, Name, Line, E),
		    cleanup(Module, Options)
		)
	    )
	;   Result = setup_failed(Unit, Name, Line)
	).


%%	result_vars(+Expected, -Vars) is det.
%
%	Create a term v(V1, ...) containing all variables at the left
%	side of the comparison operator on Expected.

result_vars(Expected, Vars) :-
	arg(1, Expected, CmpOp),
	arg(1, CmpOp, Vars).

%%	nondet_compare(+Expected, +Bindings, +Unit, +Name, +Line) is semidet.
%
%	Compare list/set results for non-deterministic predicates.
%
%	@tbd	Properly report errors
%	@bug	Sort should deal with equivalence on the comparison
%		operator.

nondet_compare(all(Cmp), Bindings, _Unit, _Name, _Line) :-
	cmp(Cmp, _Vars, Op, Values),
	cmp_list(Values, Bindings, Op).
nondet_compare(set(Cmp), Bindings0, _Unit, _Name, _Line) :-
	cmp(Cmp, _Vars, Op, Values0),
	sort(Bindings0, Bindings),
	sort(Values0, Values),
	cmp_list(Values, Bindings, Op).

cmp_list([], [], _Op).
cmp_list([E0|ET], [V0|VT], Op) :-
	call(Op, E0, V0),
	cmp_list(ET, VT, Op).

%%	cmp(+CmpTerm, -Left, -Op, -Right) is det.

cmp(Var  == Value, Var,  ==, Value).
cmp(Var =:= Value, Var, =:=, Value).
cmp(Var  =  Value, Var,  =,  Value).
:- if(swi).
cmp(Var =@= Value, Var, =@=, Value).
:- else.
:- if(sicstus).
cmp(Var =@= Value, Var, variant, Value). % variant/2 is the same =@=
:- endif.
:- endif.


%%	call_det(:Goal, -Det) is nondet.
%
%	True if Goal succeeded.  Det is unified to =true= if Goal left
%	no choicepoints and =false= otherwise.

:- if((swi|sicstus)).
call_det(Goal, Det) :-
	call_cleanup(Goal,Det0=true),
	( var(Det0) -> Det = false ; Det = true ).
:- else.
call_det(Goal, true) :-
	call(Goal).
:- endif.

%%	match_error(+Expected, +Received) is semidet.
%
%	True if the Received errors matches the expected error. Matching
%	is based on subsumes_term/2.

match_error(Expect, Rec) :-
	subsumes_term(Expect, Rec).

%%	setup(+Module, +Context, +Options) is semidet.
%
%	Call the setup handler and  fail  if   it  cannot  run  for some
%	reason. The condition handler is  similar,   but  failing is not
%	considered an error.  Context is one of
%
%	    * unit(Unit)
%	    If it is the setup handler for a unit
%	    * test(Unit,Name,Line)
%	    If it is the setup handler for a test

setup(Module, Context, Options) :-
	option(condition(Condition), Options),
	option(setup(Setup), Options), !,
	setup(Module, Context, [condition(Condition)]),
	setup(Module, Context, [setup(Setup)]).
setup(Module, Context, Options) :-
	option(setup(Setup), Options), !,
	(   catch(call_ex(Module, Setup), E, true)
	->  (   var(E)
	    ->	true
	    ;	print_message(error, plunit(error(setup, Context, E))),
		fail
	    )
	;   print_message(error, error(goal_failed(Setup), _)),
	    fail
	).
setup(Module, Context, Options) :-
	option(condition(Setup), Options), !,
	(   catch(call_ex(Module, Setup), E, true)
	->  (   var(E)
	    ->	true
	    ;	print_message(error, plunit(error(condition, Context, E))),
		fail
	    )
	;   fail
	).
setup(_,_,_).

%%	call_ex(+Module, +Goal)
%
%	Call Goal in Module after applying goal expansion.

call_ex(Module, Goal) :-
	Module:(expand_goal(Goal, GoalEx),
		GoalEx).

%%	cleanup(+Module, +Options) is det.
%
%	Call the cleanup handler and succeed.   Failure  or error of the
%	cleanup handler is reported, but tests continue normally.

cleanup(Module, Options) :-
	option(cleanup(Cleanup), Options, true),
	(   catch(call_ex(Module, Cleanup), E, true)
	->  (   var(E)
	    ->	true
	    ;	print_message(warning, E)
	    )
	;   print_message(warning, goal_failed(Cleanup, '(cleanup handler)'))
	).

success(Unit, Name, Line, Det, _Time, Options) :-
	memberchk(fixme(Reason), Options), !,
	(   (   Det == true
	    ;	memberchk(nondet, Options)
	    )
	->  put_char(user_error, +),
	    Ok = passed
	;   put_char(user_error, !),
	    Ok = nondet
	),
	flush_output(user_error),
	assert(fixme(Unit, Name, Line, Reason, Ok)).
success(Unit, Name, Line, _, _, Options) :-
	failed_assertion(Unit, Name, Line, _,_,_,_), !,
	failure(Unit, Name, Line, assertion, Options).
success(Unit, Name, Line, Det, Time, Options) :-
	assert(passed(Unit, Name, Line, Det, Time)),
	(   (   Det == true
	    ;	memberchk(nondet, Options)
	    )
	->  put_char(user_error, .)
	;   unit_file(Unit, File),
	    print_message(warning, plunit(nondet(File, Line, Name)))
	),
	flush_output(user_error).

failure(Unit, Name, Line, _, Options) :-
	memberchk(fixme(Reason), Options), !,
	put_char(user_error, -),
	flush_output(user_error),
	assert(fixme(Unit, Name, Line, Reason, failed)).
failure(Unit, Name, Line, E, Options) :-
	report_failure(Unit, Name, Line, E, Options),
	assert_cyclic(failed(Unit, Name, Line, E)).

%%	assert_cyclic(+Term) is det.
%
%	Assert  a  possibly  cyclic  unit   clause.  Current  SWI-Prolog
%	assert/1 does not handle cyclic terms,  so we emulate this using
%	the recorded database.
%
%	@tbd	Implement cycle-safe assert and remove this.

:- if(swi).
assert_cyclic(Term) :-
	acyclic_term(Term), !,
	assert(Term).
assert_cyclic(Term) :-
	Term =.. [Functor|Args],
	recorda(cyclic, Args, Id),
	functor(Term, _, Arity),
	length(NewArgs, Arity),
	Head =.. [Functor|NewArgs],
	assert((Head :- recorded(_, Var, Id), Var = NewArgs)).
:- else.
:- if(sicstus).
:- endif.
assert_cyclic(Term) :-
	assert(Term).
:- endif.


		 /*******************************
		 *	      REPORTING		*
		 *******************************/

%%	begin_test(Unit, Test, Line, STO) is det.
%%	end_test(Unit, Test, Line, STO) is det.
%
%	Maintain running/5 and report a test has started/is ended using
%	a =silent= message:
%
%	    * plunit(begin(Unit:Test, File:Line, STO))
%	    * plunit(end(Unit:Test, File:Line, STO))
%
%	@see message_hook/3 for intercepting these messages

begin_test(Unit, Test, Line, STO) :-
	thread_self(Me),
	assert(running(Unit, Test, Line, STO, Me)),
	unit_file(Unit, File),
	print_message(silent, plunit(begin(Unit:Test, File:Line, STO))).

end_test(Unit, Test, Line, STO) :-
	thread_self(Me),
	retractall(running(_,_,_,_,Me)),
	unit_file(Unit, File),
	print_message(silent, plunit(end(Unit:Test, File:Line, STO))).

%%	running_tests is det.
%
%	Print the currently running test.

running_tests :-
	running_tests(Running),
	print_message(informational, plunit(running(Running))).

running_tests(Running) :-
	findall(running(Unit:Test, File:Line, STO, Thread),
		(   running(Unit, Test, Line, STO, Thread),
		    unit_file(Unit, File)
		), Running).


%%	check_for_test_errors is semidet.
%
%	True if there are no errors, otherwise false.

check_for_test_errors :-
	number_of_clauses(failed/4, Failed),
	number_of_clauses(failed_assertion/7, FailedAssertion),
	number_of_clauses(sto/4, STO),
	Failed+FailedAssertion+STO =:= 0.     % fail on errors


%%	report is det.
%
%	Print a summary of the tests that ran.

report :-
	number_of_clauses(passed/5, Passed),
	number_of_clauses(failed/4, Failed),
	number_of_clauses(failed_assertion/7, FailedAssertion),
	number_of_clauses(blocked/4, Blocked),
	number_of_clauses(sto/4, STO),
	(   Passed+Failed+FailedAssertion+Blocked+STO =:= 0
	->  info(plunit(no_tests))
	;   Failed+FailedAssertion+Blocked+STO =:= 0
	->  report_fixme,
	    info(plunit(all_passed(Passed)))
	;   report_blocked,
	    report_fixme,
	    report_failed_assertions,
	    report_failed,
	    report_sto,
	    info(plunit(passed(Passed)))
	).

number_of_clauses(F/A,N) :-
	(   current_predicate(F/A)
	->  functor(G,F,A),
	    findall(t, G, Ts),
	    length(Ts, N)
	;   N = 0
	).

report_blocked :-
	number_of_clauses(blocked/4,N),
	N > 0, !,
	info(plunit(blocked(N))),
	(   blocked(Unit, Name, Line, Reason),
	    unit_file(Unit, File),
	    print_message(informational,
			  plunit(blocked(File:Line, Name, Reason))),
	    fail ; true
	).
report_blocked.

report_failed :-
	number_of_clauses(failed/4, N),
	info(plunit(failed(N))).

report_failed_assertions :-
	number_of_clauses(failed_assertion/7, N),
	info(plunit(failed_assertions(N))).

report_sto :-
	number_of_clauses(sto/4, N),
	info(plunit(sto(N))).

report_fixme :-
	report_fixme(_,_,_).

report_fixme(TuplesF, TuplesP, TuplesN) :-
	fixme(failed, TuplesF, Failed),
	fixme(passed, TuplesP, Passed),
	fixme(nondet, TuplesN, Nondet),
	print_message(informational, plunit(fixme(Failed, Passed, Nondet))).


fixme(How, Tuples, Count) :-
	findall(fixme(Unit, Name, Line, Reason, How),
		fixme(Unit, Name, Line, Reason, How), Tuples),
	length(Tuples, Count).


report_failure(_, _, _, assertion, _) :- !,
	put_char(user_error, 'A').
report_failure(Unit, Name, Line, Error, _Options) :-
	print_message(error, plunit(failed(Unit, Name, Line, Error))).


%%	test_report(What) is det.
%
%	Produce reports on test results after the run.

test_report(fixme) :- !,
	report_fixme(TuplesF, TuplesP, TuplesN),
	append([TuplesF, TuplesP, TuplesN], Tuples),
	print_message(informational, plunit(fixme(Tuples))).
test_report(What) :-
	throw_error(domain_error(report_class, What), _).


		 /*******************************
		 *	       INFO		*
		 *******************************/

%%	current_test_set(?Unit) is nondet.
%
%	True if Unit is a currently loaded test-set.

current_test_set(Unit) :-
	current_unit(Unit, _Module, _Context, _Options).

%%	unit_file(+Unit, -File) is det.
%%	unit_file(-Unit, +File) is nondet.

unit_file(Unit, File) :-
	current_unit(Unit, Module, _Context, _Options),
	current_module(Module, File).
unit_file(Unit, PlFile) :-
	nonvar(PlFile),
	test_file_for(TestFile, PlFile),
	current_module(Module, TestFile),
	current_unit(Unit, Module, _Context, _Options).


		 /*******************************
		 *	       FILES		*
		 *******************************/

%%	load_test_files(+Options) is det.
%
%	Load .plt test-files related to loaded source-files.

load_test_files(_Options) :-
	(   source_file(File),
	    file_name_extension(Base, Old, File),
	    Old \== plt,
	    file_name_extension(Base, plt, TestFile),
	    exists_file(TestFile),
	    (	test_file_for(TestFile, File)
	    ->	true
	    ;	load_files(TestFile,
			   [ if(changed),
			     imports([])
			   ]),
		asserta(test_file_for(TestFile, File))
	    ),
	    fail ; true
	).



		 /*******************************
		 *	     MESSAGES		*
		 *******************************/

%%	info(+Term)
%
%	Runs print_message(Level, Term), where Level  is one of =silent=
%	or =informational= (default).

info(Term) :-
	message_level(Level),
	print_message(Level, Term).

message_level(Level) :-
	current_test_flag(test_options, Options),
	option(silent(Silent), Options, false),
	(   Silent == false
	->  Level = informational
	;   Level = silent
	).

locationprefix(File:Line) --> !,
	[ '~w:~d:\n\t'-[File,Line]].
locationprefix(test(Unit,_Test,Line)) --> !,
	{ unit_file(Unit, File) },
	locationprefix(File:Line).
locationprefix(unit(Unit)) --> !,
	[ 'PL-Unit: unit ~w: '-[Unit] ].
locationprefix(FileLine) -->
	{ throw_error(type_error(locationprefix,FileLine), _) }.

:- discontiguous
	message//1.

message(error(context_error(plunit_close(Name, -)), _)) -->
	[ 'PL-Unit: cannot close unit ~w: no open unit'-[Name] ].
message(error(context_error(plunit_close(Name, Start)), _)) -->
	[ 'PL-Unit: cannot close unit ~w: current unit is ~w'-[Name, Start] ].
message(plunit(nondet(File, Line, Name))) -->
	locationprefix(File:Line),
	[ 'PL-Unit: Test ~w: Test succeeded with choicepoint'- [Name] ].
message(error(plunit(incompatible_options, Tests), _)) -->
	[ 'PL-Unit: incompatible test-options: ~p'-[Tests] ].

					% Unit start/end
:- if(swi).
message(plunit(begin(Unit))) -->
	[ 'PL-Unit: ~w '-[Unit], flush ].
message(plunit(end(_Unit))) -->
	[ at_same_line, ' done' ].
:- else.
message(plunit(begin(Unit))) -->
	[ 'PL-Unit: ~w '-[Unit]/*, flush-[]*/ ].
message(plunit(end(_Unit))) -->
	[ ' done'-[] ].
:- endif.
message(plunit(blocked(unit(Unit, Reason)))) -->
	[ 'PL-Unit: ~w blocked: ~w'-[Unit, Reason] ].
message(plunit(running([]))) --> !,
	[ 'PL-Unit: no tests running' ].
message(plunit(running([One]))) --> !,
	[ 'PL-Unit: running ' ],
	running(One).
message(plunit(running(More))) --> !,
	[ 'PL-Unit: running tests:', nl ],
	running(More).
message(plunit(fixme([]))) --> !.
message(plunit(fixme(Tuples))) --> !,
	fixme_message(Tuples).

					% Blocked tests
message(plunit(blocked(1))) --> !,
	[ 'one test is blocked:'-[] ].
message(plunit(blocked(N))) -->
	[ '~D tests are blocked:'-[N] ].
message(plunit(blocked(Pos, Name, Reason))) -->
	locationprefix(Pos),
	test_name(Name),
	[ ': ~w'-[Reason] ].

					% fail/success
message(plunit(no_tests)) --> !,
	[ 'No tests to run' ].
message(plunit(all_passed(1))) --> !,
	[ 'test passed' ].
message(plunit(all_passed(Count))) --> !,
	[ 'All ~D tests passed'-[Count] ].
message(plunit(passed(Count))) --> !,
	[ '~D tests passed'-[Count] ].
message(plunit(failed(0))) --> !,
	[].
message(plunit(failed(1))) --> !,
	[ '1 test failed'-[] ].
message(plunit(failed(N))) -->
	[ '~D tests failed'-[N] ].
message(plunit(failed_assertions(0))) --> !,
	[].
message(plunit(failed_assertions(1))) --> !,
	[ '1 assertion failed'-[] ].
message(plunit(failed_assertions(N))) -->
	[ '~D assertions failed'-[N] ].
message(plunit(sto(0))) --> !,
	[].
message(plunit(sto(N))) -->
	[ '~D test results depend on unification mode'-[N] ].
message(plunit(fixme(0,0,0))) -->
	[].
message(plunit(fixme(Failed,0,0))) --> !,
	[ 'all ~D tests flagged FIXME failed'-[Failed] ].
message(plunit(fixme(Failed,Passed,0))) -->
	[ 'FIXME: ~D failed; ~D passed'-[Failed, Passed] ].
message(plunit(fixme(Failed,Passed,Nondet))) -->
	{ TotalPassed is Passed+Nondet },
	[ 'FIXME: ~D failed; ~D passed; (~D nondet)'-
	  [Failed, TotalPassed, Nondet] ].
message(plunit(failed(Unit, Name, Line, Failure))) -->
       { unit_file(Unit, File) },
       locationprefix(File:Line),
       test_name(Name),
       [': '-[] ],
       failure(Failure).
:- if(swi).
message(plunit(failed_assertion(Unit, Name, Line, AssertLoc,
				_STO, Reason, Goal))) -->
       { unit_file(Unit, File) },
       locationprefix(File:Line),
       test_name(Name),
       [ ': assertion'-[] ],
       assertion_location(AssertLoc, File),
       assertion_reason(Reason), ['\n\t'],
       assertion_goal(Unit, Goal).

assertion_location(File:Line, File) -->
	[ ' at line ~w'-[Line] ].
assertion_location(File:Line, _) -->
	[ ' at ~w:~w'-[File, Line] ].
assertion_location(unknown, _) -->
	[].

assertion_reason(fail) --> !,
	[ ' failed'-[] ].
assertion_reason(Error) -->
	{ message_to_string(Error, String) },
	[ ' raised "~w"'-[String] ].

assertion_goal(Unit, Goal) -->
	{ unit_module(Unit, Module),
	  unqualify(Goal, Module, Plain)
	},
	[ 'Assertion: ~p'-[Plain] ].

unqualify(Var, _, Var) :-
	var(Var), !.
unqualify(M:Goal, Unit, Goal) :-
	nonvar(M),
	unit_module(Unit, M), !.
unqualify(M:Goal, _, Goal) :-
	callable(Goal),
	predicate_property(M:Goal, imported_from(system)), !.
unqualify(Goal, _, Goal).

:- endif.
					% Setup/condition errors
message(plunit(error(Where, Context, Exception))) -->
	locationprefix(Context),
	{ message_to_string(Exception, String) },
	[ 'error in ~w: ~w'-[Where, String] ].

					% STO messages
message(plunit(sto(Unit, Name, Line))) -->
	{ unit_file(Unit, File) },
       locationprefix(File:Line),
       test_name(Name),
       [' is subject to occurs check (STO): '-[] ].
message(plunit(sto(Type, Result))) -->
	sto_type(Type),
	sto_result(Result).

					% Interrupts (SWI)
:- if(swi).
message(interrupt(begin)) -->
	{ thread_self(Me),
	  running(Unit, Test, Line, STO, Me), !,
	  unit_file(Unit, File)
	},
	[ 'Interrupted test '-[] ],
	running(running(Unit:Test, File:Line, STO, Me)),
	[nl],
	'$messages':prolog_message(interrupt(begin)).
message(interrupt(begin)) -->
	'$messages':prolog_message(interrupt(begin)).
:- endif.

test_name(@(Name,Bindings)) --> !,
	[ 'test ~w (forall bindings = ~p)'-[Name, Bindings] ].
test_name(Name) --> !,
	[ 'test ~w'-[Name] ].

sto_type(sto_error_incomplete) -->
	[ 'Finite trees (error checking): ' ].
sto_type(rational_trees) -->
	[ 'Rational trees: ' ].
sto_type(finite_trees) -->
	[ 'Finite trees: ' ].

sto_result(success(_Unit, _Name, _Line, Det, Time)) -->
	det(Det),
	[ ' success in ~2f seconds'-[Time] ].
sto_result(failure(_Unit, _Name, _Line, How)) -->
	failure(How).

det(true) -->
	[ 'deterministic' ].
det(false) -->
	[ 'non-deterministic' ].

running(running(Unit:Test, File:Line, STO, Thread)) -->
	thread(Thread),
	[ '~q:~q at ~w:~d'-[Unit, Test, File, Line] ],
	current_sto(STO).
running([H|T]) -->
	['\t'], running(H),
	(   {T == []}
	->  []
	;   [nl], running(T)
	).

thread(main) --> !.
thread(Other) -->
	[' [~w] '-[Other] ].

current_sto(sto_error_incomplete) -->
	[ ' (STO: error checking)' ].
current_sto(rational_trees) -->
	[].
current_sto(finite_trees) -->
	[ ' (STO: occurs check enabled)' ].

:- if(swi).
write_term(T, OPS) -->
	['~@'-[write_term(T,OPS)]].
:- else.
write_term(T, _OPS) -->
	['~q'-[T]].
:- endif.

expected_got_ops_(Ex, E, OPS, Goals) -->
	['    Expected: '-[]], write_term(Ex, OPS), [nl],
	['    Got:      '-[]], write_term(E,  OPS), [nl],
	( { Goals = [] } -> []
	; ['       with: '-[]], write_term(Goals, OPS), [nl]
	).


failure(Var) -->
	{ var(Var) }, !,
	[ 'Unknown failure?' ].
failure(succeeded(Time)) --> !,
	[ 'must fail but succeeded in ~2f seconds~n'-[Time] ].
failure(wrong_error(Expected, Error)) --> !,
	{ copy_term(Expected-Error, Ex-E, Goals),
	  numbervars(Ex-E-Goals, 0, _),
	  write_options(OPS)
	},
	[ 'wrong error'-[], nl ],
	expected_got_ops_(Ex, E, OPS, Goals).
failure(wrong_answer(Cmp)) -->
	{ Cmp =.. [Op,Answer,Expected], !,
	  copy_term(Expected-Answer, Ex-A, Goals),
	  numbervars(Ex-A-Goals, 0, _),
	  write_options(OPS)
	},
	[ 'wrong answer (compared using ~w)'-[Op], nl ],
	expected_got_ops_(Ex, A, OPS, Goals).
failure(wrong_answer(CmpExpected, Bindings)) -->
	{ (   CmpExpected = all(Cmp)
	  ->  Cmp =.. [_Op1,_,Expected],
	      Got = Bindings,
	      Type = all
	  ;   CmpExpected = set(Cmp),
	      Cmp =.. [_Op2,_,Expected0],
	      sort(Expected0, Expected),
	      sort(Bindings, Got),
	      Type = set
	  )
	},
	[ 'wrong "~w" answer:'-[Type] ],
	[ nl, '    Expected: ~q'-[Expected] ],
	[ nl, '       Found: ~q'-[Got] ].
:- if(swi).
failure(cmp_error(_Cmp, Error)) -->
	{ message_to_string(Error, Message) },
	[ 'Comparison error: ~w'-[Message] ].
failure(Error) -->
	{ Error = error(_,_), !,
	  message_to_string(Error, Message)
	},
	[ 'received error: ~w'-[Message] ].
:- endif.
failure(Why) -->
	[ '~p~n'-[Why] ].

fixme_message([]) --> [].
fixme_message([fixme(Unit, _Name, Line, Reason, How)|T]) -->
	{ unit_file(Unit, File) },
	fixme_message(File:Line, Reason, How),
	(   {T == []}
	->  []
	;   [nl],
	    fixme_message(T)
	).

fixme_message(Location, Reason, failed) -->
	[ 'FIXME: ~w: ~w'-[Location, Reason] ].
fixme_message(Location, Reason, passed) -->
	[ 'FIXME: ~w: passed ~w'-[Location, Reason] ].
fixme_message(Location, Reason, nondet) -->
	[ 'FIXME: ~w: passed (nondet) ~w'-[Location, Reason] ].


write_options([ numbervars(true),
		quoted(true),
		portray(true),
		max_depth(10),
		attributes(portray)
	      ]).

:- if(swi).

:- multifile
	prolog:message/3,
	user:message_hook/3.

prolog:message(Term) -->
	message(Term).

%	user:message_hook(+Term, +Kind, +Lines)

user:message_hook(make(done(Files)), _, _) :-
	make_run_tests(Files),
	fail.				% give other hooks a chance

:- endif.

:- if(sicstus).

user:generate_message_hook(Message) -->
	message(Message),
	[nl].				% SICStus requires nl at the end

%%	user:message_hook(+Severity, +Message, +Lines) is semidet.
%
%	Redefine printing some messages. It appears   SICStus has no way
%	to get multiple messages at the same   line, so we roll our own.
%	As there is a lot pre-wired and   checked in the SICStus message
%	handling we cannot reuse the lines. Unless I miss something ...

user:message_hook(informational, plunit(begin(Unit)), _Lines) :-
	format(user_error, '% PL-Unit: ~w ', [Unit]),
	flush_output(user_error).
user:message_hook(informational, plunit(end(_Unit)), _Lines) :-
	format(user, ' done~n', []).

:- endif.
