Prolog Example Unit Tests

These small examples are designed to run quickly
with the Gerrit prolog-shell, but not depending
on a local Gerrit repository server.

Change-Id: I8f58a6740c6f2c79ae1314f2ae593409ee60440d
This commit is contained in:
Chih-Hung Hsieh
2019-04-01 20:05:54 -07:00
parent 3d92a9ab06
commit bde955c276
11 changed files with 521 additions and 0 deletions

View File

@@ -74,6 +74,9 @@ For interactive testing and playing with Prolog, Gerrit provides the
link:pgm-prolog-shell.html[prolog-shell] program which opens an interactive
Prolog interpreter shell.
For batch or unit tests, see the examples in Gerrit source directory
link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/prologtests/examples/[prologtests/examples].
[NOTE]
The interactive shell is just a prolog shell, it does not load
a gerrit server environment and thus is not intended for

View File

@@ -0,0 +1,7 @@
package(default_visibility = ["//visibility:public"])
sh_test(
name = "test_examples",
srcs = ["run.sh"],
data = glob(["*.pl"]) + ["//:gerrit.war"],
)

View File

@@ -0,0 +1,54 @@
# Prolog Unit Test Examples
## Run all examples
Build a local gerrit.war and then run the script:
./run.sh
Note that a local Gerrit server is not needed because
these unit test examples redefine wrappers of the `gerrit:change\*`
rules to provide mocked change data.
## Add a new unit test
Please follow the pattern in `t1.pl`, `t2.pl`, or `t3.pl`.
* Put code to be tested in a file, e.g. `rules.pl`.
For easy unit testing, split long clauses into short ones
and test every positive and negative path.
* Create a new unit test file, e.g. `t1.pl`,
which should _load_ the test source file and `utils.pl`.
% First load all source files and the utils.pl.
:- load([aosp_rules,utils]).
:- begin_tests(t1). % give this test any name
% Use test0/1 or test1/1 to verify failed/passed goals.
:- end_tests(_,0). % check total pass/fail counts
* Optionally replace calls to gerrit functions that depend on repository.
For example, define the following wrappers and in source code, use
`change_branch/1` instead of `gerrti:change_branch/1`.
change_branch(X) :- gerrit:change_branch(X).
commit_label(L,U) :- gerrit:commit_label(L,U).
* In unit test file, redefine the gerrit function wrappers and test.
For example, in `t3.pl`, we have:
:- redefine(uploader,1,uploader(user(42))). % mocked uploader
:- test1(uploader(user(42))).
:- test0(is_exempt_uploader).
% is_exempt_uploader/0 is expected to fail because it is
% is_exempt_uploader :- uploader(user(Id)), memberchk(Id, [104, 106]).
% Note that gerrit:remove_label does not depend on Gerrit repository,
% so its caller remove_label/1 is tested without any redefinition.
:- test1(remove_label('MyReview',[],[])).
:- test1(remove_label('MyReview',submit(),submit())).

View File

@@ -0,0 +1,148 @@
% A simplified and mocked AOSP rules.pl
%%%%% wrapper functions for unit tests
change_branch(X) :- gerrit:change_branch(X).
change_project(X) :- gerrit:change_project(X).
commit_author(U,N,M) :- gerrit:commit_author(U,N,M).
commit_delta(X) :- gerrit:commit_delta(X).
commit_label(L,U) :- gerrit:commit_label(L,U).
uploader(X) :- gerrit:uploader(X).
%%%%% true/false conditions
% Special auto-merger accounts.
is_exempt_uploader :-
uploader(user(Id)),
memberchk(Id, [104, 106]).
% Build cop overrides everything.
has_build_cop_override :-
commit_label(label('Build-Cop-Override', 1), _).
is_exempt_from_reviews :-
or(is_exempt_uploader, has_build_cop_override).
% Some files in selected projects need API review.
needs_api_review :-
commit_delta('^(.*/)?api/|^(system-api/)'),
change_project(Project),
memberchk(Project, [
'platform/external/apache-http',
'platform/frameworks/base',
'platform/frameworks/support',
'platform/packages/services/Car',
'platform/prebuilts/sdk'
]).
% Some branches need DrNo review.
needs_drno_review :-
change_branch(Branch),
memberchk(Branch, [
'refs/heads/my-alpha-dev',
'refs/heads/my-beta-dev'
]).
% Some author email addresses need Qualcomm-Review.
needs_qualcomm_review :-
commit_author(_, _, M),
regex_matches(
'.*@(qti.qualcomm.com|qca.qualcomm.com|quicinc.com|qualcomm.com)', M).
% Special projects, branches, user accounts
% can opt out owners review.
opt_out_find_owners :-
change_branch(Branch),
memberchk(Branch, [
'refs/heads/my-beta-testing',
'refs/heads/my-testing'
]).
% Special projects, branches, user accounts
% can opt in owners review.
% Note that opt_out overrides opt_in.
opt_in_find_owners :- true.
%%%%% Simple list filters.
remove_label(X, In, Out) :-
gerrit:remove_label(In, label(X, _), Out).
% Slow but simple for short input list.
remove_review_categories(In, Out) :-
remove_label('API-Review', In, L1),
remove_label('Code-Review', L1, L2),
remove_label('DrNo-Review', L2, L3),
remove_label('Owner-Review-Vote', L3, L4),
remove_label('Qualcomm-Review', L4, L5),
remove_label('Verified', L5, Out).
%%%%% Missing rules in Gerrit Prolog Cafe.
or(InA, InB) :- once((A;B)).
not(Goal) :- Goal -> false ; true.
% memberchk(+Element, +List)
memberchk(X, [H|T]) :-
(X = H -> true ; memberchk(X, T)).
maplist(Functor, In, Out) :-
(In = []
-> Out = []
; (In = [X1|T1],
Out = [X2|T2],
Goal =.. [Functor, X1, X2],
once(Goal),
maplist(Functor, T1, T2)
)
).
%%%%% Conditional rules and filters.
submit_filter(In, Out) :-
(is_exempt_from_reviews
-> remove_review_categories(In, Out)
; (check_review(needs_api_review,
'API_Review', In, L1),
check_review(needs_drno_review,
'DrNo-Review', L1, L2),
check_review(needs_qualcomm_review,
'Qualcomm-Review', L2, L3),
check_find_owners(L3, Out)
)
).
check_review(NeedReview, Label, In, Out) :-
(NeedReview
-> Out = In
; remove_label(Label, In, Out)
).
% If opt_out_find_owners is true,
% remove all 'Owner-Review-Vote' label;
% else if opt_in_find_owners is true,
% call find_owners:submit_filter;
% else default to no find_owners filter.
check_find_owners(In, Out) :-
(opt_out_find_owners
-> remove_label('Owner-Review-Vote', In, Temp)
; (opt_in_find_owners
-> find_owners:submit_filter(In, Temp)
; In = Temp
)
),
Temp =.. [submit | L1],
remove_label('Owner-Approved', L1, L2),
maplist(owner_may_to_need, L2, L3),
Out =.. [submit | L3].
% change may(_) to need(_) to block submit.
owner_may_to_need(In, Out) :-
(In = label('Owner-Review-Vote', may(_))
-> Out = label('Owner-Review-Vote', need(_))
; Out = In
).

View File

@@ -0,0 +1,26 @@
% If you have 1.4.3 or older Prolog-Cafe, you need to
% use (consult(load), load(load)) to get definition of load.
% Then use load([f1,f2,...]) to load multiple source files.
% Input is a list of file names or a single file name.
% Use a conditional expression style without cut operator.
load(X) :-
( (X = [])
-> true
; ( (X = [H|T])
-> (load_file(H), load(T))
; load_file(X)
)
).
% load_file is '$consult' without the bug of unbound 'File' variable.
% For repeated unit tests, skip statistics and print_message.
load_file(F) :- atom(F), !,
'$prolog_file_name'(F, PF),
open(PF, read, In),
% print_message(info, [loading,PF,'...']),
% statistics(runtime, _),
consult_stream(PF, In),
% statistics(runtime, [_,T]),
% print_message(info, [PF,'loaded in',T,msec]),
close(In).

View File

@@ -0,0 +1,29 @@
% An example source file to be tested.
% Add common rules missing in Prolog Cafe.
memberchk(X, [H|T]) :-
(X = H) -> true ; memberchk(X, T).
% A rule that can succeed/backtrack multiple times.
super_users(1001).
super_users(1002).
% Deterministic rule that pass/fail only once.
is_super_user(X) :- memberchk(X, [1001, 1002]).
% Another rule that can pass 5 times.
multi_users(101).
multi_users(102).
multi_users(103).
multi_users(104).
multi_users(105).
% Okay, single deterministic fact.
single_user(abc).
% Wrap calls to gerrit repository, to be redefined in tests.
change_owner(X) :- gerrit:change_owner(X).
% To test is_owner without gerrit:change_owner,
% we should redefine change_owner.
is_owner(X) :- change_owner(X).

62
prologtests/examples/run.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
TESTS="t1 t2 t3"
# Note that both t1.pl and t2.pl test code in rules.pl.
# Unit tests are usually longer than the tested code.
# So it is common to test one source file with multiple
# unit test files.
LF=$'\n'
PASS=""
FAIL=""
echo "#### TEST_SRCDIR = ${TEST_SRCDIR}"
if [ "${TEST_SRCDIR}" == "" ]; then
# Assume running alone
GERRIT_WAR="../../bazel-bin/gerrit.war"
SRCDIR="."
else
# Assume running from bazel
GERRIT_WAR=`pwd`/gerrit.war
SRCDIR="prologtests/examples"
fi
# Default GERRIT_TMP is ~/.gerritcodereview/tmp,
# which won't be writable in a bazel test sandbox.
/bin/mkdir -p /tmp/gerrit
export GERRIT_TMP=/tmp/gerrit
for T in $TESTS
do
pushd $SRCDIR
# Unit tests do not need to define clauses in packages.
# Use one prolog-shell per unit test, to avoid name collision.
echo "### Running test ${T}.pl"
echo "[$T]." | java -jar ${GERRIT_WAR} prolog-shell -q -s load.pl
if [ "x$?" != "x0" ]; then
echo "### Test ${T}.pl failed."
FAIL="${FAIL}${LF}FAIL: Test ${T}.pl"
else
PASS="${PASS}${LF}PASS: Test ${T}.pl"
fi
popd
# java -jar ../../bazel-bin/gerrit.war prolog-shell -s $T < /dev/null
# Calling prolog-shell with -s flag works for small files,
# but got run-time exception with t3.pl.
# com.googlecode.prolog_cafe.exceptions.ReductionLimitException:
# exceeded reduction limit of 1048576
done
echo "$PASS"
if [ "$FAIL" != "" ]; then
echo "$FAIL"
exit 1
fi

View File

@@ -0,0 +1,20 @@
:- load([rules,utils]).
:- begin_tests(t1).
:- test1(true). % expect true to pass
:- test0(false). % expect false to fail
:- test1(X = 3). % unification should pass
:- test1(_ = 3). % unification should pass
:- test0(X \= 3). % not-unified should fail
% (7-4) should have expected result
:- test1((X is (7-4), X =:= 3)).
:- test1((X is (7-4), X =\= 4)).
% memberchk should pass/fail exactly once
:- test1(memberchk(3,[1,3,5,3])).
:- test0(memberchk(2,[1,3,5,3])).
:- test0(memberchk(2,[])).
:- end_tests_or_halt(0). % expect no failure

View File

@@ -0,0 +1,25 @@
:- load([rules,utils]).
:- begin_tests(t2).
% expected to pass or fail once.
:- test0(super_users(1000)).
:- test1(super_users(1001)).
:- test1(is_super_user(1001)).
:- test1(is_super_user(1002)).
:- test0(is_super_user(1003)).
:- test1(super_users(X)). % expected fail (pass twice)
:- test1(multi_users(X)). % expected fail (pass many times)
:- test1(single_user(X)). % expected pass once
% Redefine change_owner, skip gerrit:change_owner,
% then test is_owner without a gerrit repository.
:- redefine(change_owner,1,(change_owner(42))).
:- test1(is_owner(42)).
:- test1(is_owner(X)).
:- test0(is_owner(24)).
:- end_tests_or_halt(2). % expect 2 failures

View File

@@ -0,0 +1,69 @@
:- load([aosp_rules,utils]).
:- begin_tests(t3_basic_conditions).
%% A negative test of is_exempt_uploader.
:- redefine(uploader,1,uploader(user(42))). % mocked uploader
:- test1(uploader(user(42))).
:- test0(is_exempt_uploader).
%% Helper functions for positive test of is_exempt_uploader.
test_is_exempt_uploader(List) :- maplist(test1_uploader, List, _).
test1_uploader(X,_) :-
redefine(uploader,1,uploader(user(X))),
test1(uploader(user(X))),
test1(is_exempt_uploader).
:- test_is_exempt_uploader([104, 106]).
%% Test has_build_cop_override.
:- redefine(commit_label,2,commit_label(label('Code-Review',1),user(102))).
:- test0(has_build_cop_override).
commit_label(label('Build-Cop-Override',1),user(101)). % mocked 2nd label
:- test1(has_build_cop_override).
:- test1(commit_label(label(_,_),_)). % expect fail, two matches
:- test1(commit_label(label('Build-Cop-Override',_),_)). % good, one pass
%% TODO: more test for is_exempt_from_reviews.
%% Test needs_api_review, which checks commit_delta and project.
% Helper functions:
test_needs_api_review(File, Project, Tester) :-
redefine(commit_delta,1,(commit_delta(R) :- regex_matches(R, File))),
redefine(change_project,1,change_project(Project)),
Goal =.. [Tester, needs_api_review],
msg('# check CL with changed file ', File, ' in ', Project),
once((Goal ; true)). % do not backtrack
:- test_needs_api_review('apio/test.cc', 'platform/art', test0).
:- test_needs_api_review('api/test.cc', 'platform/art', test0).
:- test_needs_api_review('api/test.cc', 'platform/prebuilts/sdk', test1).
:- test_needs_api_review('d1/d2/api/test.cc', 'platform/prebuilts/sdk', test1).
:- test_needs_api_review('system-api/d/t.c', 'platform/external/apache-http', test1).
%% TODO: Test needs_drno_review, needs_qualcomm_review
%% TODO: Test opt_out_find_owners.
:- test1(opt_in_find_owners). % default, unless opt_out_find_owners
:- end_tests_or_halt(1). % expect 1 failure of multiple commit_label
%% Test remove_label
:- begin_tests(t3_remove_label).
:- test1(remove_label('MyReview',[],[])).
:- test1(remove_label('MyReview',submit(),submit())).
:- test1(remove_label(myR,[label(a,X)],[label(a,X)])).
:- test1(remove_label(myR,[label(myR,_)],[])).
:- test1(remove_label(myR,[label(a,X),label(myR,_)],[label(a,X)])).
:- test1(remove_label(myR,submit(label(a,X)),submit(label(a,X)))).
:- test1(remove_label(myR,submit(label(myR,_)),submit())).
%% Test maplist
double(X,Y) :- Y is X * X.
:- test1(maplist(double, [2,4,6], [4,16,36])).
:- test1(maplist(double, [], [])).
:- end_tests_or_halt(0). % expect no failure
%% TODO: Add more tests.

View File

@@ -0,0 +1,78 @@
%% Unit test helpers
% Write one line message.
msg(A) :- write(A), nl.
msg(A,B) :- write(A), msg(B).
msg(A,B,C) :- write(A), msg(B,C).
msg(A,B,C,D) :- write(A), msg(B,C,D).
msg(A,B,C,D,E) :- write(A), msg(B,C,D,E).
msg(A,B,C,D,E,F) :- write(A), msg(B,C,D,E,F).
% Redefine a caluse.
redefine(Atom,Arity,Clause) :- abolish(Atom/Arity), assertz(Clause).
% Increment/decrement of pass/fail counters.
set_counters(N,X,Y) :- redefine(test_count,3,test_count(N,X,Y)).
get_counters(N,X,Y) :- clause(test_count(N,X,Y), _) -> true ; (X=0, Y=0).
inc_pass_count :- get_counters(N,P,F), P1 is P + 1, set_counters(N,P1,F).
inc_fail_count :- get_counters(N,P,F), F1 is F + 1, set_counters(N,P,F1).
% Report pass or fail of G.
pass_1(G) :- msg('PASS: ', G), inc_pass_count.
fail_1(G) :- msg('FAIL: ', G), inc_fail_count.
% Report pass or fail of not(G).
pass_0(G) :- msg('PASS: not(', G, ')'), inc_pass_count.
fail_0(G) :- msg('FAIL: not(', G, ')'), inc_fail_count.
% Report a test as failed if it passed 2 or more times
pass_twice(G) :-
msg('FAIL: (pass twice): ', G),
inc_fail_count.
pass_many(G) :-
G = [A,B|_],
length(G, N),
msg('FAIL: (pass ', N, ' times): ', [A,B,'...']),
inc_fail_count.
% Test if G fails.
test0(G) :- once(G) -> fail_0(G) ; pass_0(G).
% Test if G passes exactly once.
test1(G) :-
findall(G, G, S), length(S, N),
(N == 0
-> fail_1(G)
; (N == 1
-> pass_1(S)
; (N == 2 -> pass_twice(S) ; pass_many(S))
)
).
% Report the begin of test N.
begin_tests(N) :-
nl,
msg('BEGIN test ',N),
set_counters(N,0,0).
% Repot the end of test N and total pass/fail counts,
% and check if the numbers are as exected OutP/OutF.
end_tests(OutP,OutF) :-
get_counters(N,P,F),
(OutP = P
-> msg('Expected #PASS: ', OutP)
; (msg('ERROR: expected #PASS is ',OutP), !, fail)
),
(OutF = F
-> msg('Expected #FAIL: ', OutF)
; (msg('ERROR: expected #FAIL is ',OutF), !, fail)
),
msg('END test ', N),
nl.
% Repot the end of test N and total pass/fail counts.
end_tests(N) :- end_tests(N,_,_).
% Call end_tests/2 and halt if the fail count is unexpected.
end_tests_or_halt(ExpectedFails) :-
end_tests(_,ExpectedFails); (flush_output, halt(1)).