[m-dev.] conditional compilation
Peter Wang
novalazy at gmail.com
Wed Mar 25 16:02:38 AEDT 2009
2009/3/25 Ralph Becket <rafe at csse.unimelb.edu.au>:
> Peter Wang, Wednesday, 25 March 2009:
>> Hi,
>>
>> Here is another idea: conditional sections, where a section is kept or
>> omitted depending on compile time flags and/or grades. If we borrow the
>> feature from trace goals, we might have something like this:
>>
>> :- implementation(not(flag("production"))).
>>
>> I can think of a couple of uses.
>>
>> 1. I had an idea to ease writing test cases at MC. One thing it would allow
>> is embedding test code in the same file as the module it is testing, which
>> may have certain advantages (or not, that's not the point here). We could
>> strip out test code for production builds.
>
> That sounds appealing.
There's not much to it.
%-----------------------------------------------------------------------------%
%
% Testing framework.
%
% OVERVIEW
%
% Use a :- initialise predicate in each module to add test cases, using
% `add_tests'.
%
% In the main/2 predicate of an application, or a dedicated driver program,
% call `maybe_run_tests'. If the environment variable `RUNTESTS' is set (see
% below) then test cases from imported modules will be run. Otherwise you can
% continue normal execution of the program.
%
% ENVIRONMENT
%
% The RUNTESTS variable can be set to any of the following to execute all
% known tests: '', 'all', 'yes', 'true', 'y', '1'
%
% Otherwise it should be a whitespace-separated list of "specs", each of
% which has one of the following forms:
%
% <group>
% <group>:<test>
%
% which will execute either all the tests in the group, or just a single test
% case.
%
%-----------------------------------------------------------------------------%
:- module test_framework.
:- interface.
:- import_module assoc_list.
:- import_module bool.
:- import_module io.
%-----------------------------------------------------------------------------%
:- type test_pred == (pred(io, io)).
:- inst test_pred == (pred(di, uo) is det).
% add_tests(Group, Tests, !IO)
%
% Register a group of tests. This is designed to be run from a module's
% :- initialise predicate.
%
% The group name groups together related tests; `$module' would be a good
% choice. Large modules may be broken into multiple groups.
% Tests is an association list of Name - TestPred.
%
% A test case that throws an uncaught exception when executed counts as a
% failing case; otherwise it is counted as a success. Test cases may write
% output to standard output.
%
:- pred add_tests(string::in, assoc_list(string, test_pred)::in,
io::di, io::uo) is det.
% maybe_run_tests(RanTests, !IO)
%
% This predicate is designed to be run from the `main/2' predicate of a
% real application, or a test driver program.
% If RanTests is `yes' then you should not continue normal execution of the
% application but exit after cleaning up.
%
:- pred maybe_run_tests(bool::out, io::di, io::uo) is det.
% Like maybe_run_tests, but pass the spec string directly instead of
% reading it from the environment. This may be more useful for test
% drivers.
%
:- pred run_tests(string::in, io::di, io::uo) is det.
%-----------------------------------------------------------------------------%
%
% Utilities for writing tests
%
% You can throw any type of exception, but this one is a decent default
% and is treated a bit specially.
%
:- type test_failure
---> test_failure(string, int).
% Succeed if the first two arguments are equal, otherwise throw an
% exception.
%
:- pred expect_equal(T::in, T::in, string::in, int::in) is det.
% Succeed if the first two arguments are not, otherwise throw an
% exception.
%
:- pred expect_not_equal(T::in, T::in, string::in, int::in) is det.
%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%
:- implementation.
:- import_module exception.
:- import_module int.
:- import_module list.
:- import_module map.
:- import_module maybe.
:- import_module pair.
:- import_module pretty_printer.
:- import_module string.
:- import_module univ.
%-----------------------------------------------------------------------------%
:- type known_tests == map(string, assoc_list(string, test_pred)).
:- type tally
---> tally(
tally_num_tests :: int,
tally_run :: int,
tally_failed :: int,
tally_skipped_spec :: int
).
%-----------------------------------------------------------------------------%
% We can't rely on mutables as module initialisers for other modules can be run
% before our mutable initialiser. So we have to invent our own mutables using
% foreign code :(
:- pragma foreign_decl("C", local, "
static MR_Word known_tests;
").
:- pragma foreign_code("C", "
static MR_Word known_tests = 0; /* relies on representation of map.empty */
").
:- pred get_known_tests(known_tests::out, io::di, io::uo) is det.
:- pragma foreign_proc("C",
get_known_tests(Map::out, IO0::di, IO::uo),
[will_not_call_mercury, promise_pure, thread_safe],
"
Map = known_tests;
IO = IO0;
").
:- pred set_known_tests(known_tests::in, io::di, io::uo) is det.
:- pragma foreign_proc("C",
set_known_tests(Map::in, IO0::di, IO::uo),
[will_not_call_mercury, promise_pure, thread_safe],
"
known_tests = Map;
IO = IO0;
").
%-----------------------------------------------------------------------------%
add_tests(Group, Tests, !IO) :-
get_known_tests(KnownTests0, !IO),
( map.search(KnownTests0, Group, List0) ->
List = List0 ++ Tests,
map.det_update(KnownTests0, Group, List, KnownTests)
;
map.det_insert(KnownTests0, Group, Tests, KnownTests)
),
set_known_tests(KnownTests, !IO).
%-----------------------------------------------------------------------------%
maybe_run_tests(RanTests, !IO) :-
io.get_environment_var("RUNTESTS", MaybeRuntests, !IO),
(
MaybeRuntests = yes(Specs),
run_tests(Specs, !IO),
RanTests = yes
;
MaybeRuntests = no,
% Save memory for the rest of the execution.
set_known_tests(map.init, !IO),
RanTests = no
).
run_tests(Specs, !IO) :-
get_known_tests(KnownTests, !IO),
Words0 = string.words(Specs),
(
( Words0 = []
; Words0 = ["all"]
; Words0 = ["yes"]
; Words0 = ["true"]
; Words0 = ["y"]
; Words0 = ["1"]
)
->
io.write_string("% RUNNING ALL TESTS\n", !IO),
Words = map.keys(KnownTests)
;
Words = Words0
),
list.foldl(count_tests(KnownTests), Words, 0, NumTests),
Tally0 = tally(NumTests, 0, 0, 0),
list.foldl2(run_tests_by_spec(KnownTests), Words, Tally0, Tally, !IO),
report_tally(Tally, !IO).
:- pred count_tests(known_tests::in, string::in, int::in, int::out) is det.
count_tests(KnownTests, Spec, !Acc) :-
SpecWords = string.split_at_char(':', Spec),
(
SpecWords = [GroupName],
( map.search(KnownTests, GroupName, TestGroup) ->
!:Acc = !.Acc + list.length(TestGroup)
;
true
)
;
SpecWords = [GroupName, TestName],
(
map.search(KnownTests, GroupName, TestGroup),
assoc_list.search(TestGroup, TestName, _TestPred)
->
!:Acc = !.Acc + 1
;
true
)
;
( SpecWords = []
; SpecWords = [_, _, _ | _]
)
).
:- pred run_tests_by_spec(known_tests::in, string::in, tally::in, tally::out,
io::di, io::uo) is det.
run_tests_by_spec(KnownTests, Spec, !Tally, !IO) :-
SpecWords = string.split_at_char(':', Spec),
(
SpecWords = [GroupName],
( map.search(KnownTests, GroupName, TestGroup) ->
io.write_string("% RUNNING GROUP ", !IO),
io.write_string(GroupName, !IO),
io.nl(!IO),
run_test_group(GroupName, TestGroup, !Tally, !IO)
;
io.write_string("% SKIPPED ", !IO),
io.write_string(GroupName, !IO),
io.write_string(" (group not found)\n", !IO),
bad_spec(!Tally, !IO)
)
;
SpecWords = [GroupName, TestName],
(
map.search(KnownTests, GroupName, TestGroup),
assoc_list.search(TestGroup, TestName, TestPred)
->
run_single_test(GroupName, TestName - TestPred, !Tally, !IO)
;
io.write_string("% SKIPPED ", !IO),
io.write_string(Spec, !IO),
io.write_string(" (test or group not found)\n", !IO),
bad_spec(!Tally, !IO)
)
;
( SpecWords = []
; SpecWords = [_, _, _ | _]
),
io.write_string("% SKIPPED ", !IO),
io.write_string(Spec, !IO),
io.write_string(" (syntax error)\n", !IO),
bad_spec(!Tally, !IO)
).
:- pred run_test_group(string::in, assoc_list(string, test_pred)::in,
tally::in, tally::out, io::di, io::uo) is det.
run_test_group(Group, Tests, !Tally, !IO) :-
list.foldl2(run_single_test(Group), Tests, !Tally, !IO).
:- pred bad_spec(tally::in, tally::out, io::di, io::uo) is det.
bad_spec(!Tally, !IO) :-
!Tally ^ tally_skipped_spec := !.Tally ^ tally_skipped_spec + 1,
io.set_exit_status(1, !IO).
%-----------------------------------------------------------------------------%
:- pred run_single_test(string::in, pair(string, test_pred)::in,
tally::in, tally::out, io::di, io::uo) is det.
run_single_test(Group, Name - PredGround, !Tally, !IO) :-
report_running(Group, Name, !.Tally, !IO),
promise_equivalent_solutions [!:Tally, !:IO] (
try_io(run_single_test_2(PredGround), Result, !IO),
!Tally ^ tally_run := !.Tally ^ tally_run + 1,
(
Result = succeeded({}),
report_succeeded(Group, Name, !IO)
;
Result = exception(Excp),
!Tally ^ tally_failed := !.Tally ^ tally_failed + 1,
io.set_exit_status(1, !IO),
report_failed(Group, Name, Excp, !IO)
)
),
% Blank line to make it easier to skip over irrelevant cases in a report.
io.nl(!IO).
:- pred run_single_test_2(test_pred::in, {}::out, io::di, io::uo) is det.
run_single_test_2(PredGround, {}, !IO) :-
cast_test_pred(PredGround, Pred),
Pred(!IO).
:- pred cast_test_pred(test_pred::in, test_pred::out(test_pred)) is det.
:- pragma foreign_proc("C",
cast_test_pred(PredGround::in, Pred::out(test_pred)),
[will_not_call_mercury, promise_pure, thread_safe],
"
Pred = PredGround;
").
:- pred report_running(string::in, string::in, tally::in,
io::di, io::uo) is det.
report_running(Group, Name, Tally, !IO) :-
ruler(!IO),
io.write_string("% RUNNING TEST ", !IO),
write_test_name(Group, Name, !IO),
io.write_string(" (", !IO),
io.write_int(Tally ^ tally_run + 1, !IO),
io.write_char('/', !IO),
io.write_int(Tally ^ tally_num_tests, !IO),
io.write_string(")\n", !IO).
:- pred report_succeeded(string::in, string::in, io::di, io::uo) is det.
report_succeeded(Group, Name, !IO) :-
io.write_string("% PASSED TEST ", !IO),
write_test_name(Group, Name, !IO),
io.nl(!IO).
:- pred report_failed(string::in, string::in, univ::in, io::di, io::uo) is det.
report_failed(Group, TestName, Excp, !IO) :-
io.write_string("% UNCAUGHT EXCEPTION\n", !IO),
( univ_to_type(Excp, test_failure(Name, Line)) ->
io.write_string("test failure at ", !IO),
io.write_string(Name, !IO),
io.write_string(", line ", !IO),
io.write_int(Line, !IO),
io.nl(!IO)
;
pretty_printer.write_doc(format(univ_value(Excp)), !IO),
io.nl(!IO)
),
io.write_string("% FAILED TEST ", !IO),
write_test_name(Group, TestName, !IO),
io.nl(!IO).
:- pred write_test_name(string::in, string::in, io::di, io::uo) is det.
write_test_name(Group, Name, !IO) :-
io.write_string(Group, !IO),
io.write_char(':', !IO),
io.write_string(Name, !IO).
:- pred report_tally(tally::in, io::di, io::uo) is det.
report_tally(Tally, !IO) :-
Tally = tally(_NumTests, RunTests, FailedTests, SkippedSpecs),
ruler(!IO),
io.write_string("% SUMMARY\n", !IO),
io.write_string("% Passed: ", !IO),
io.write_int(RunTests - FailedTests, !IO),
io.nl(!IO),
io.write_string("% Failed: ", !IO),
io.write_int(FailedTests, !IO),
io.nl(!IO),
( SkippedSpecs > 0 ->
io.write_string("% Skipped specs: ", !IO),
io.write_int(SkippedSpecs, !IO),
io.nl(!IO)
;
true
).
:- pred ruler(io::di, io::uo) is det.
ruler(!IO) :-
io.write_string("%--------------------------------------------------%\n",
!IO).
%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%
expect_equal(T1, T2, Name, Line) :-
trace [io(!IO)] (
io.write_string("expect ", !IO),
io.write(T1, !IO),
io.write_string(" = ", !IO),
io.write(T2, !IO),
io.nl(!IO)
),
( T1 = T2 ->
true
;
throw(test_failure(Name, Line))
).
expect_not_equal(T1, T2, Name, Line) :-
trace [io(!IO)] (
io.write_string("expect ", !IO),
io.write(T1, !IO),
io.write_string(" \\= ", !IO),
io.write(T2, !IO),
io.nl(!IO)
),
( T1 \= T2 ->
true
;
throw(test_failure(Name, Line))
).
%-----------------------------------------------------------------------------%
% vim: ft=mercury ts=8 sts=4 sw=4 et
--------------------------------------------------------------------------
mercury-developers mailing list
Post messages to: mercury-developers at csse.unimelb.edu.au
Administrative Queries: owner-mercury-developers at csse.unimelb.edu.au
Subscriptions: mercury-developers-request at csse.unimelb.edu.au
--------------------------------------------------------------------------
More information about the developers
mailing list