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]).
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.
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]).
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]).
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]).
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]).
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
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]).
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]).
This is analogous to isNull()
or isNone()
in other xUnit implementations
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]).
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', []).
This checks for list/set membership, and also whether Var is a valid dictionary key in Collection
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]).
This checks for list/set membership, and also whether Var is a valid dictionary key in Collection
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]).
Supported types are: number, integer, float, atom, compound, list, dict
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]).
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
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]).
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]).
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]).
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]).
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 ------------------------------------------------------------------
440assert_test_fails(Goal) :-
441 (
442 -> feedback('Asserted test failure but test passed: ~q', [Goal]),
443 fail
444 ; true
445 ).
452assert_test_passes(Goal) :- 453 . 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).
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.