1:- module(plunit_assert, [
    2    assert_equals/2,
    3    assert_not_equals/2,
    4    assert_gt/2,
    5    assert_gte/2,
    6    assert_lt/2,
    7    assert_lte/2,
    8    assert_is/2,
    9    assert_is_not/2,
   10    assert_false/1,
   11    assert_true/1,
   12    assert_unbound/1,
   13    assert_not_unbound/1,
   14    assert_in/2,
   15    assert_not_in/2,
   16    assert_type/2,
   17    assert_not_type/2,
   18    assert_output/3,
   19    % Meta stuff - not really part of the plunit_assert API
   20    % assert_test_feedback/2,
   21    assert_test_fails/1,
   22    assert_test_passes/1
   23]).

The test API for plunit_assert

A unit testing library for Prolog, providing an expressive xUnit-like API for PlUnit, and better feedback to the user on test fail.

author
- Simon Harding <github@pointbeing.net>
license
- MIT */
   32:- dynamic prolog:assertion_failed/2.   33:- multifile prolog:assertion_failed/2.   34:- discontiguous plunit_assert:assert_type/2.   35:- discontiguous plunit_assert:assert_not_type/2.
 assert_true(:Goal) is semidet
Test that Goal succeeds and therefore is truthy
Arguments:
Goal- The goal to be tested
See also
- assertion/1
   43assert_true(Cond) :-
   44    call_protected(Cond, fail_assert_true(Cond)).
   45
   46fail_assert_true(Goal) :-
   47    feedback('Asserted true but got false for: ~q', [Goal]).
 assert_false(:Goal) is semidet
Test that Goal fails and therefore is falsy
Arguments:
Goal- The goal to be tested
See also
- assert_true/1
   55assert_false(Cond) :-
   56    call_protected((\+ Cond), fail_assert_false(Cond)).
   57
   58fail_assert_false(Goal) :-
   59    feedback('Asserted false but got true for: ~q', [Goal]).
 assert_equals(+A, +B) is semidet
This is a superset of assert_is/2 and arithmetic comparison with =:=
Arguments:
A- The first of the terms to be compared
B- The second of the terms to be compared
   67assert_equals(A, B) :-
   68    call_protected(A == B; A =:= B, fail_assert_equals(A, B)).
   69
   70fail_assert_equals(A, B) :-
   71    pretty_with_eval(A, PA),
   72    pretty_with_eval(B, PB),
   73    feedback('Asserted equal but ~w and ~w are not equal', [PA, PB]).
 assert_not_equals(+A, +B) is semidet
Test that A and B are not equal terms
Arguments:
A- The first of the terms to be compared
B- The second of the terms to be compared
See also
- assert_equals/2
   82assert_not_equals(A, B) :-
   83    call_protected(\+ equal_after_eval(A, B),
   84                   fail_assert_not_equals(A, B)).
   85
   86% this differs somewhat from assert_equals/2 at the moment, but this general
   87% approach may be more suitable once we start comparing compound terms etc
   88equal_after_eval(A, B) :-
   89    catch(ValA is A, _, fail),
   90    catch(ValB is B, _, fail),
   91    ValA =:= ValB, !.
   92equal_after_eval(A, B) :-
   93    A == B.
   94
   95fail_assert_not_equals(A, B) :-
   96    pretty_with_eval(A, PA),
   97    pretty_with_eval(B, PB),
   98    feedback('Asserted ~w and ~w are not equal, but they are', [PA, PB]).
 assert_is(+A, +B) is semidet
Test that A and B are identical terms

Uses ==/2 to check for term identity, which means it compares the terms A and B structurally, including the functor and arity (number of arguments) of the terms and the equality of each corresponding argument. Thus, succeeds if A and B are identical terms, without attempting to unify variables or perform any arithmetic evaluations

Arguments:
A- The first of the terms to be compared
B- The second of the terms to be compared
See also
- assertion/1
- ==/2
  114assert_is(A, B) :-
  115    call_protected(A == B, fail_assert_is(A, B)).
  116
  117fail_assert_is(A, B) :-
  118    feedback('Asserted identity but ~q and ~q are not identical', [A, B]).
 assert_is_not(+A, +B) is semidet
Test that A and B are not identical terms
Arguments:
A- The first of the terms to be compared
B- The second of the terms to be compared
See also
- assert_is/2
  127assert_is_not(A, B) :-
  128    call_protected(A \== B, fail_assert_is_not(A, B)).
  129
  130fail_assert_is_not(A, B) :-
  131    feedback('Asserted ~q and ~q are not identical, but they are', [A, B]).
 assert_unbound(+Var) is semidet
Test that Var is unbound

This is analogous to isNull() or isNone() in other xUnit implementations

Arguments:
Var- The variable to be tested for boundness
See also
- assertion/1
  141assert_unbound(Var) :-
  142    call_protected(var(Var), fail_assert_unbound(Var)).
  143
  144fail_assert_unbound(Var) :-
  145    feedback('Assertion that variable is unbound failed: it was bound to ~w', [Var]).
 assert_not_unbound(+Var) is semidet
Test that Var is not unbound
Arguments:
Var- The variable to be tested for unboundness
See also
- assert_unbound/1
  153assert_not_unbound(Var) :-
  154    call_protected(nonvar(Var), fail_assert_not_unbound(Var)).
  155
  156fail_assert_not_unbound(_) :-
  157    feedback('Assertion that variable is bound failed: it was unbound', []).
 assert_in(+Var, +Collection) is semidet
Test that Var is in Collection

This checks for list/set membership, and also whether Var is a valid dictionary key in Collection

Arguments:
Var- The needle
Collection- The haystack
See also
- assertion/1
  169assert_in(Var, Collection) :-
  170    call_protected((
  171        member(Var, Collection) ;
  172        get_dict(Var, Collection, _)
  173    ), fail_assert_in(Var, Collection)).
  174
  175fail_assert_in(Var, Collection) :-
  176    feedback('Asserted ~w is in ~w, but it isn\'t', [Var, Collection]).
 assert_not_in(+Var, +Collection) is semidet
Test that Var is not in Collection

This checks for list/set membership, and also whether Var is a valid dictionary key in Collection

Arguments:
Var- The needle
Collection- The haystack
See also
- assert_in/2
  188assert_not_in(Var, Collection) :-
  189    call_protected(\+ (
  190        member(Var, Collection) ;
  191        ( is_dict(Collection), get_dict(Var, Collection, _) )
  192    ), fail_assert_not_in(Var, Collection)).
  193
  194fail_assert_not_in(Var, Collection) :-
  195    feedback('Asserted ~w is not in ~w, but it is', [Var, Collection]).
 assert_type(+Term, +Type) is semidet
Test that Var is of type Type

Supported types are: number, integer, float, atom, compound, list, dict

Arguments:
Term- The term to be tested
Type- The type to be asserted
See also
- assertion/1
To be done
- Compound types
- Consider must_be/2 or similar
  208assert_type(Term, boolean) :- call_protected(is_boolean(Term), fail_assert_type(boolean, Term)), !.
  209assert_type(Term, float) :- call_protected(float(Term), fail_assert_type(float, Term)), !.
  210assert_type(Term, integer) :- call_protected(integer(Term), fail_assert_type(integer, Term)), !.
  211assert_type(Term, number) :- call_protected(number(Term), fail_assert_type(number, Term)), !.
  212assert_type(Term, atom) :- call_protected(atom(Term), fail_assert_type(atom, Term)), !.
  213assert_type(Term, compound) :- call_protected(compound(Term), fail_assert_type(compound, Term)), !.
  214assert_type(Term, list) :- call_protected(is_list(Term), fail_assert_type(list, Term)), !.
  215assert_type(Term, dict) :- call_protected(is_dict(Term), fail_assert_type(dict, Term)), !.
  216assert_type(Term, string) :- call_protected(string(Term), fail_assert_type(string, Term)), !.
  217
  218fail_assert_type(Expected, Term) :-
  219    term_type(Term, Got),
  220    feedback('Asserted ~w is of type \'~w\' but got \'~w\'', [Term, Expected, Got]).
  221
  222% and for specific compounds...
  223assert_type(Term, Expected) :-
  224    compound(Term),
  225    \+ base_type(Expected),
  226    functor(Term, Got, _),
  227    call_protected(Expected == Got, fail_assert_type_compound(Expected, Term, Got)), !.
  228
  229fail_assert_type_compound(Expected, Term, Got) :-
  230    feedback('Asserted compound ~w is of type \'~w\' but got \'~w\'', [Term, Expected, Got]).
  231
  232% and finally, some made up type that doesn't exist
  233assert_type(Term, Expected) :-
  234    \+ compound(Term),
  235    \+ base_type(Expected),
  236    fail_assert_type_not_found(Expected),
  237    fail.
  238
  239fail_assert_type_not_found(Expected) :-
  240    feedback('Assertion failed: \'~q\' is not a known type', [Expected]).
 assert_not_type(+Term, +Type) is semidet
Test that Var is not of type Type
Arguments:
Term- The term to be tested
Type- The type to be un-asserted
See also
- assert_type/2
  249assert_not_type(Term, boolean) :- call_protected(\+ is_boolean(Term), fail_assert_not_type(boolean, Term)), !.
  250assert_not_type(Term, float) :- call_protected(\+ float(Term), fail_assert_not_type(float, Term)), !.
  251assert_not_type(Term, integer) :- call_protected(\+ integer(Term), fail_assert_not_type(integer, Term)), !.
  252assert_not_type(Term, number) :- call_protected(\+ number(Term), fail_assert_not_type(number, Term)), !.
  253assert_not_type(Term, atom) :- call_protected(\+ atom(Term), fail_assert_not_type(atom, Term)), !.
  254assert_not_type(Term, compound) :- call_protected(\+ compound(Term), fail_assert_not_type(compound, Term)), !.
  255assert_not_type(Term, list) :- call_protected(\+ is_list(Term), fail_assert_not_type(list, Term)), !.
  256assert_not_type(Term, dict) :- call_protected(\+ is_dict(Term), fail_assert_not_type(dict, Term)), !.
  257assert_not_type(Term, string) :- call_protected(\+ string(Term), fail_assert_not_type(string, Term)), !.
  258
  259fail_assert_not_type(Expected, Term) :-
  260    feedback('Asserted ~w is not of type \'~w\', but it is', [Term, Expected]).
  261
  262% and finally, some made up type that doesn't exist
  263assert_not_type(Term, Expected) :-
  264    \+ compound(Term),
  265    \+ base_type(Expected),
  266    fail_assert_type_not_found(Expected),
  267    fail.
  268
  269% TODO:
  270% could add assert_not_type/2 for specific compounds, but I don't see a case for it
 assert_gt(+A, +B) is semidet
Test that A is greater than B
Arguments:
A-
B-
  278assert_gt(A, B) :-
  279    call_protected(A > B, fail_assert_gt(A, B)).
  280
  281fail_assert_gt(A, B) :-
  282    pretty_with_eval(A, PA),
  283    pretty_with_eval(B, PB),
  284    feedback('Comparison failed: ~w is not greater than ~', [PA, PB]).
 assert_lt(+A, +B) is semidet
Test that A is less than B
Arguments:
A-
B-
  292assert_lt(A, B) :-
  293    call_protected(A < B, fail_assert_lt(A, B)).
  294
  295fail_assert_lt(A, B) :-
  296    pretty_with_eval(A, PA),
  297    pretty_with_eval(B, PB),
  298    feedback('Comparison failed: ~w is not less than ~w', [PA, PB]).
 assert_gte(+A, +B) is semidet
Test that A is greater than or equal to B
Arguments:
A-
B-
  306assert_gte(A, B) :-
  307    call_protected(A >= B, fail_assert_gte(A, B)).
  308
  309fail_assert_gte(A, B) :-
  310    pretty_with_eval(A, PA),
  311    pretty_with_eval(B, PB),
  312    feedback('Comparison failed: ~w is not greater than or equal to ~w', [PA, PB]).
 assert_lte(+A, +B) is semidet
Test that A is less than or equal to B
Arguments:
A-
B-
  320assert_lte(A, B) :-
  321    call_protected(A =< B, fail_assert_lte(A, B)).
  322
  323fail_assert_lte(A, B) :-
  324    pretty_with_eval(A, PA),
  325    pretty_with_eval(B, PB),
  326    feedback('Comparison failed: ~w is not less than or equal to ~w', [PA, PB]).
 assert_output(:Goal, +Vars:list, +Expected:list) is semidet
Test that a predicate's output arguments match what is expected
Arguments:
Goal- The predicate to be invoked
Got- The list of vars to be inspected
Expected- The expected values for Vars
  335assert_output(Goal, Vars, Expected) :-
  336    call(Goal),
  337    find_values(Vars, Actual),
  338    call_protected(Actual == Expected, fail_assert_output(Expected, Actual)),
  339    !.
  340
  341find_values([], []).
  342find_values([V|Vs], [Val|Vals]) :-
  343    Val = V,
  344    find_values(Vs, Vals).
  345
  346fail_assert_output(Expected, Actual) :-
  347    feedback('Output does not match expected: expected ~w, got ~w', [Expected, Actual]).
  348
  349% TODO: report on individual vars
  350% compare_vars([], []) :- !.
  351% compare_vars([V|Vs], [E|Es]) :-
  352%     (   V == E
  353%     ->  true
  354%     ;   throw(error(pa_assertion_failed(V, E), _))
  355%     ),
  356%     compare_vars(Vs, Es).
  357
  358
  359% private rules ---------------------------------------------------------------
  360
  361
  362base_type(atom).
  363base_type(boolean).
  364base_type(dict).
  365base_type(float).
  366base_type(integer).
  367base_type(list).
  368base_type(number).
  369base_type(string).
  370%base_type(variable). % not really, but it prevents assert_type/2 testing it as a compound
  371
  372term_type(Term, Type) :-
  373    (   var(Term) -> Type = variable
  374    ;   atom(Term) -> Type = atom
  375    ;   compound(Term) -> Type = compound
  376    ;   is_dict(Term) -> Type = dict
  377    ;   float(Term) -> Type = float
  378    ;   integer(Term) -> Type = integer
  379    ;   is_list(Term) -> Type = list
  380    % number would never hit by this point
  381    ;   string(Term) -> Type = string
  382    %;   Type = unknown
  383    ).
  384
  385feedback(Format, Args) :-
  386    format(atom(Atom), Format, Args),
  387    format(user_error, '[plunit_assert] ~s', [Atom]).
  388
  389call_protected(Cond, Callback) :-
  390    setup_call_cleanup(
  391        (asserta((prolog:assertion_failed(_, _) :-
  392                    nb_setval(at_assertion_failed_val, true),
  393                    call(Callback)),
  394                Ref),
  395         nb_setval(at_assertion_failed_val, false)
  396        ),
  397        (catch(assertion(Cond), _, true),
  398         nb_getval(at_assertion_failed_val, Failed)
  399         ),
  400        erase(Ref)
  401    ),
  402    Failed == false.
  403/*
  404pretty_with_eval(Term, Pretty) :-
  405    (
  406        % Try to evaluate as arithmetic
  407        term_type(Term, TermType),
  408        \+ base_type(TermType),
  409        catch(Result is Term, _, fail
  410    )
  411    -> format(atom(Pretty), '~q (~q)', [Term, Result])
  412    ; format(atom(Pretty), '~q', [Term])
  413    ).
  414*/
  415
  416pretty_with_eval(Term, Pretty) :-
  417    (
  418        term_type(Term, TermType),
  419        \+ base_type(TermType),
  420        catch(Result is Term, _, fail)
  421    ->  (   Result == Term
  422        ->  term_string(Term, Pretty)        % no need to show both
  423        ;   format(string(Pretty), "~w (~w)", [Term, Result])
  424        )
  425    ;   term_string(Term, Pretty)
  426    ).
  427
  428is_boolean(Term) :-
  429    (Term == true; Term == false).
  430
  431
  432% meta-tests ------------------------------------------------------------------
 assert_test_fails(:Goal) is semidet
Meta test to check that Goal would trigger a PlUnit test fail
Arguments:
Goal- The goal to be queried in the form of a plunit_assert predicate
  440assert_test_fails(Goal) :-
  441    (   Goal
  442    ->  feedback('Asserted test failure but test passed: ~q', [Goal]),
  443        fail
  444    ;   true
  445    ).
 assert_test_passes(:Goal) is semidet
Meta test to check that Goal would not trigger a PlUnit test fail
Arguments:
Goal- The goal to be queried in the form of a plunit_assert predicate
  452assert_test_passes(Goal) :-
  453    Goal.
  454
  455% These don't work. See #20
  456
  457% assert_test_feedback(TestGoal, Expected) :-
  458%     with_output_to(atom(Actual), catch(TestGoal, _, true)),
  459%     assert_equals(Actual, Expected).
  460
  461% assert_test_feedback(TestGoal, Expected) :-
  462%     current_output(OldOut),
  463%     with_output_to(atom(Actual), (
  464%         set_output(user_error),
  465%         catch(TestGoal, _, true),
  466%         flush_output(user_error)
  467%     )),
  468%     set_output(OldOut),
  469%     assert_equals(Expected, Actual).