Use Prolog Cafe for ChangeControl.canSubmit()

Re-implement the canSubmit() logic in Prolog, removing one of the
last uses of FunctionState.  This is a simple re-implementation
that does not provide any new functionality.

PatchSetApproval objects are exposed into Prolog on demand by
loading them from the database if the submit predicate needs
them. These get indexed in a temporary HashMap by their label name
(aka ApprovalCategory), making access faster during evaluation of
the rule.

ApprovalTypes are loaded into Prolog on demand from the Guice
Injector.  This will eventually go away when we get rid of the
global declaration of ApprovalCategories in the web UI.

CurrentUser instances are loaded and cached within the Prolog
environment as needed to consider PatchSetApproval objects and if
the user has the necessary permission to make that assertion.

Bug: 971
Change-Id: I7e261948db08b7c3180e590e81f492ff3e6f237e
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2011-06-06 13:49:06 -07:00
parent 7b8c92b5a5
commit 9f2535231b
20 changed files with 1376 additions and 34 deletions

View File

@@ -0,0 +1,161 @@
// Copyright (C) 2011 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.rules;
import com.google.inject.Guice;
import com.google.inject.Module;
import com.googlecode.prolog_cafe.compiler.CompileException;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;
import junit.framework.TestCase;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Base class for any tests written in Prolog. */
public abstract class PrologTestCase extends TestCase {
private static final SymbolTerm test_1 = SymbolTerm.makeSymbol("test", 1);
private String pkg;
private boolean hasSetup;
private boolean hasTeardown;
private List<Term> tests;
protected PrologEnvironment env;
protected void load(String prologResource, Module... modules)
throws FileNotFoundException, CompileException {
ArrayList<Module> moduleList = new ArrayList<Module>();
moduleList.add(new PrologModule());
moduleList.addAll(Arrays.asList(modules));
PrologEnvironment.Factory factory =
Guice.createInjector(moduleList).getInstance(
PrologEnvironment.Factory.class);
env = factory.create(getClass().getClassLoader());
env.setMaxDatabaseSize(16 * 1024);
consult(getClass(), prologResource);
pkg = myPackage();
hasSetup = has("setup");
hasTeardown = has("teardown");
StructureTerm head = new StructureTerm(":",
SymbolTerm.makeSymbol(pkg),
new StructureTerm(test_1, new VariableTerm()));
tests = new ArrayList<Term>();
for (Term[] pair : env.all(Prolog.BUILTIN, "clause", head, new VariableTerm())) {
tests.add(pair[0]);
}
}
protected void consult(Class<?> clazz, String prologResource)
throws FileNotFoundException, CompileException {
URL url = clazz.getResource(prologResource);
if (url == null) {
throw new FileNotFoundException(prologResource);
}
String path = url.getPath();
if (!env.execute(Prolog.BUILTIN, "consult", SymbolTerm.makeSymbol(path))) {
throw new CompileException("Cannot consult" + path);
}
}
private String myPackage() {
String pkg = getClass().getName();
return pkg.substring(0, pkg.lastIndexOf('.'));
}
private boolean has(String name) {
StructureTerm head = SymbolTerm.makeSymbol(pkg, name, 0);
return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
}
public void testRunPrologTestCases() {
int errors = 0;
long start = System.currentTimeMillis();
for (Term test : tests) {
System.out.format("Prolog %-60s ...", removePackage(test));
System.out.flush();
if (hasSetup) {
call("setup");
}
List<Term> all = env.all(Prolog.BUILTIN, "call", test);
if (hasTeardown) {
call("teardown");
}
System.out.println(all.size() == 1 ? "OK" : "FAIL");
if (all.size() > 0 && !test.equals(all.get(0))) {
for (Term t : all) {
Term head = ((StructureTerm) removePackage(t)).args()[0];
Term[] args = ((StructureTerm) head).args();
System.out.print(" Result: ");
for (int i = 0; i < args.length; i++) {
if (0 < i) {
System.out.print(", ");
}
System.out.print(args[i]);
}
System.out.println();
}
System.out.println();
}
if (all.size() != 1) {
errors++;
}
}
long end = System.currentTimeMillis();
System.out.println("-------------------------------");
System.out.format("Prolog tests: %d, Failures: %d, Time elapsed %.3f sec",
tests.size(), errors, (end - start) / 1000.0);
System.out.println();
assertEquals("No Errors", 0, errors);
}
private void call(String name) {
StructureTerm head = SymbolTerm.makeSymbol(pkg, name, 0);
if (!env.execute(Prolog.BUILTIN, "call", head)) {
fail("Cannot invoke " + pkg + ":" + name);
}
}
private Term removePackage(Term test) {
Term name = test;
if (name.isStructure() && ":".equals(((StructureTerm) name).name())) {
name = ((StructureTerm) name).args()[1];
}
return name;
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (C) 2011 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.rules.common;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.rules.PrologTestCase;
import com.google.inject.AbstractModule;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CommonRulesTest extends PrologTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
final ApprovalTypes types = new ApprovalTypes(Arrays.asList(
codeReviewCategory(),
verifiedCategory()
));
load("common_rules_test.pl", new AbstractModule() {
@Override
protected void configure() {
bind(ApprovalTypes.class).toInstance(types);
}
});
}
private static ApprovalType codeReviewCategory() {
ApprovalCategory cat = category(0, "CRVW", "Code Review");
List<ApprovalCategoryValue> vals = newList();
vals.add(value(cat, 2, "Looks good to me, approved"));
vals.add(value(cat, 1, "Looks good to me, but someone else must approve"));
vals.add(value(cat, 0, "No score"));
vals.add(value(cat, -1, "I would prefer that you didn't submit this"));
vals.add(value(cat, -2, "Do not submit"));
return new ApprovalType(cat, vals);
}
private static ApprovalType verifiedCategory() {
ApprovalCategory cat = category(1, "VRIF", "Verified");
List<ApprovalCategoryValue> vals = newList();
vals.add(value(cat, 1, "Verified"));
vals.add(value(cat, 0, "No score"));
vals.add(value(cat, -1, "Fails"));
return new ApprovalType(cat, vals);
}
private static ApprovalCategory category(int pos, String id, String name) {
ApprovalCategory cat;
cat = new ApprovalCategory(new ApprovalCategory.Id(id), name);
cat.setPosition((short) pos);
return cat;
}
private static ArrayList<ApprovalCategoryValue> newList() {
return new ArrayList<ApprovalCategoryValue>();
}
private static ApprovalCategoryValue value(ApprovalCategory c, int v, String n) {
return new ApprovalCategoryValue(
new ApprovalCategoryValue.Id(c.getId(), (short) v),
n);
}
}

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.SystemConfig;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
@@ -317,13 +318,16 @@ public class RefControlTest extends TestCase {
}
};
PrologEnvironment.Factory envFactory = null;
GitRepositoryManager mgr = null;
Project.NameKey wildProject = new Project.NameKey("All-Projects");
ProjectControl.AssistedFactory projectControlFactory = null;
all.put(local.getProject().getNameKey(), new ProjectState(anonymousUser,
projectCache, wildProject, projectControlFactory, mgr, local));
projectCache, wildProject, projectControlFactory,
envFactory, mgr, local));
all.put(parent.getProject().getNameKey(), new ProjectState(anonymousUser,
projectCache, wildProject, projectControlFactory, mgr, parent));
projectCache, wildProject, projectControlFactory,
envFactory, mgr, parent));
return all.get(local.getProject().getNameKey());
}

View File

@@ -0,0 +1,114 @@
%% Copyright (C) 2011 The Android Open Source Project
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
:- package 'com.google.gerrit.rules.common'.
%% not_same
%%
test(not_same_success) :-
not_same(ok(a), ok(b)),
not_same(label(e, ok(a)), label(e, ok(b))).
%% get_legacy_approval_types
%%
test(get_legacy_approval_types) :-
get_legacy_approval_types(T),
T = [C, V],
C = approval_type('Code-Review', 'CRVW', 'MaxWithBlock', -2, 2),
V = approval_type('Verified', 'VRIF', 'MaxWithBlock', -1, 1).
%% commit_label
%%
test(commit_label_all) :-
findall(commit_label(L, U), commit_label(L, U), Out),
all_commit_labels(Ls),
Ls = Out.
test(commit_label_CodeReview) :-
L = label('Code-Review', _),
findall(p(L, U), commit_label(L, U), Out),
[ p(label('Code-Review', 2), test_user(bob)),
p(label('Code-Review', 2), test_user(alice)) ] == Out.
%% max_with_block
%%
test(max_with_block_success_accept_max_score) :-
max_with_block('Code-Review', -2, 2, ok(test_user(alice))).
test(max_with_block_success_reject_min_score) :-
max_with_block('You-Fail', -1, 1, reject(test_user(failer))).
test(max_with_block_success_need_suggest) :-
max_with_block('Verified', -1, 1, need(1)).
skip_test(max_with_block_success_impossible) :-
max_with_block('Code-Style', 0, 1, impossible(no_access)).
%% default_submit
%%
test(default_submit_fails) :-
findall(P, default_submit(P), All),
All = [submit(C, V)],
C = label('Code-Review', ok(test_user(alice))),
V = label('Verified', need(1)).
%% can_submit
%%
test(can_submit_ok) :-
set_commit_labels([
commit_label( label('Code-Review', 2), test_user(alice) ),
commit_label( label('Verified', 1), test_user(builder) )
]),
can_submit('com.google.gerrit.rules.common':default_submit, S),
S = ok(submit(C, V)),
C = label('Code-Review', ok(test_user(alice))),
V = label('Verified', ok(test_user(builder))).
test(can_submit_not_ready) :-
can_submit('com.google.gerrit.rules.common':default_submit, S),
S = not_ready(submit(C, V)),
C = label('Code-Review', ok(test_user(alice))),
V = label('Verified', need(1)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% Supporting Data
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
setup :-
init,
all_commit_labels(Ls),
set_commit_labels(Ls).
all_commit_labels(Ls) :-
Ls = [
commit_label( label('Code-Review', 2), test_user(alice) ),
commit_label( label('Code-Review', 2), test_user(bob) ),
commit_label( label('You-Fail', -1), test_user(failer) ),
commit_label( label('You-Fail', -1), test_user(alice) )
].
:- package user.
test_grant('Code-Review', test_user(alice), range(-2, 2)).
test_grant('Verified', test_user(builder), range(-1, 1)).
test_grant('You-Fail', test_user(alice), range(-1, 1)).
test_grant('You-Fail', test_user(failer), range(-1, 1)).