:- module(test_wizard,
	  [ make_tests/3,		% +Module, +File, +Out
	    make_test/3			% +Callable, -Module, -Test
	  ]).
:- use_module(library(time)).
:- use_module(library(lists)).
:- use_module(library(listing)).
:- use_module(library(readutil)).

/** <module> Test Generation Wizard

Tasks

	* Accumulate user queries
	* Suggest tests from user queries
*/

setting(max_time(5)).


		 /*******************************
		 *	 UNIT GENERATION	*
		 *******************************/

%%	make_tests(+Module, +File, +Out) is det.
%
%	Create tests from queries stored in File and write the tests for
%	Module to the stream Out.

make_tests(Module, File, Out) :-
	read_file_to_terms(File, Queries, []),
	findall(Test, (   member(Q, Queries),
			  make_test(Q, Module, Test)), Tests),
	(   Tests == []
	->  true
	;   format(Out, ':- begin_tests(~q).~n~n', [Module]),
	    maplist(portray_clause(Out), Tests),
	    format(Out, '~n:- end_tests(~q).~n', [Module])
	).


		 /*******************************
		 *	 TEST GENERATION	*
		 *******************************/

%%	make_test(+Query:callable, -Module, -Test:term) is det.
%
%	Generate a test from a query. Test   is  returned as a clause of
%	test/1  or  test/2  to  be   inserted  between  begin_tests  and
%	end_tests.

make_test(Query0, Module, (test(Name, Options) :- Query)) :-
	find_test_module(Query0, Module, Query),
	pred_name(Query, Name),
	setting(max_time(Max)),
	test_result(Module:Query, Max, Options).

%%	find_test_module(+QuerySpec, ?Module, -Query).
%
%	Find module to test from a query. Note that it is very common
%	for toplevel usage to rely on SWI-Prolog's DWIM.
%
%	@tbd	What if multiple modules match?  We can select the
%		local one or ask the user.

find_test_module(Var, _, _) :-
	var(Var), !, fail.
find_test_module(M:Query, M0, Query) :- !,
	M0 = M.
find_test_module(Query, M, Query) :-
	current_predicate(_, M:Query),
	\+ predicate_property(M:Query, imported_from(_M2)).

%%	pred_name(+Callable, -Name) is det.
%
%	Suggest a name for the test. In   the  plunit framework the name
%	needs not be unique, so we simply take the predicate name.

pred_name(Callable, Name) :-
	strip_module(Callable, _, Term),
	functor(Term, Name, _Arity).

%%	test_result(+Callable, +Maxtime, -Result) is det.
%
%	Try running goal and get meaningful results.  Results are:
%
%		* true(Templ == Var)
%		* fail
%		* all(Templ == Bindings)
%		* throws(Error)
%		* timeout

test_result(Callable, Maxtime, Result) :-
	term_variables(Callable, Vars),
	make_template(Vars, Templ),
	catch(call_with_time_limit(Maxtime,
				   findall(Templ-Det,
					   call_test(Callable, Det),
					   Bindings)),
	      E, true),
	(   var(E)
	->  success(Bindings, Templ, Result)
	;   error(E, Result)
	).

%%	success(+Bindings, +Templ, -Result) is det.
%
%	Create test-results from non-error cases.

success([], _, [fail]) :- !.
success([[]-true],  _, []) :- !.
success([S1-true],  Templ, [ true(Templ == S1) ]) :- !.
success([[]-false], _, [ nondet ]) :- !.
success([S1-false], Templ, [ true(Templ == S1), nondet ]) :- !.
success(ListDet, Templ, [all(Templ == List)]) :-
	strip_det(ListDet, List).

strip_det([], []).
strip_det([H-_|T0], [H|T]) :-
	strip_det(T0, T).

%%	error(+ErrorTerm, -Result)

error(Error0, [throws(Error)]) :-
	generalise_error(Error0, Error).


generalise_error(error(Formal, _), error(Formal, _)) :- !.
generalise_error(Term, Term).


%%	make_template(+Vars, -Template) is det.
%
%	Make a nice looking template

make_template([], []) :- !.
make_template([One], One) :- !.
make_template([One, Two], One-Two) :- !.
make_template(List, Vars) :-
	Vars =.. [v|List].

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

call_test(Goal, Det) :-
	Goal,
	deterministic(Det).


		 /*******************************
		 *	     COLLECT		*
		 *******************************/

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Collect toplevel queries if the Prolog flag log_query_file points to the
name of a writeable  file.  The  file   is  opened  in  append-mode  for
exclusive write to allow for concurrent   operation from multiple Prolog
systems using the same logfile.

The file is written in  UTF-8   encoding  and  using ignore_ops(true) to
ensure it can be read.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

:- multifile
	user:message_hook/3.

user:message_hook(toplevel_goal(Goal0, Bindings), _Level, _Lines) :-
	open_query_log(Out),
	bind_vars(Bindings),
	clean_goal(Goal0, Goal),
	call_cleanup(format(Out, '~W.~n', [Goal, [ numbervars(true),
						   quoted(true),
						   ignore_ops(true)
						 ]]), close(Out)),
	fail.

clean_goal(Var, _) :-
	var(Var), !, fail.
clean_goal(user:Goal, Goal) :- !.
clean_goal(Goal, Goal).

bind_vars([]).
bind_vars([Name=Var|T]) :-
	Var = '$VAR'(Name),
	bind_vars(T).

open_query_log(Out) :-
	current_prolog_flag(log_query_file, File),
	exists_file(File), !,
	open(File, append, Out,
	     [ encoding(utf8),
	       lock(write)
	     ]).
open_query_log(Out) :-
	current_prolog_flag(log_query_file, File),
	access_file(File, write), !,
	open(File, write, Out,
	     [ encoding(utf8),
	       lock(write),
	       bom(true)
	     ]),
	format(Out,
	       '/* SWI-Prolog query log.  This file contains all syntactically\n   \
	           correct queries issued in this directory.  It is used by the\n   \
		   test wizard to generate unit tests.\n\
		*/~n~n', []).