Merge changes from topic "plugin-pre-submit-validation"

* changes:
  Add SubmitRule extension point
  Extract the Prolog evaluator out of SubmitRuleEvaluator
  Adapt SubmitRuleEvaluator for the new SubmitRule interface
  Remove the Prolog reductions counter
  Remove unused SubmitRuleEvaluator#setPatchSet
  Change the Presubmit handling with multiple SubmitRecords
  Add tests for Prolog's handling of SubmitRecords
  Add SubmitRequirement to hold the presubmit conditions not met
This commit is contained in:
Patrick Hiesel
2018-03-14 10:06:05 +00:00
committed by Gerrit Code Review
31 changed files with 1300 additions and 591 deletions

View File

@@ -2731,10 +2731,114 @@ class MyCommandInterceptor implements SshCreateCommandInterceptor {
@Override
public String intercept(String in) {
return pluginName + " mycommand";
----
[[pre-submit-evaluator]]
== Pre-submit Validation Plugins
Gerrit provides an extension point that enables plugins to prevent a change
from being submitted.
[IMPORTANT]
This extension point **must NOT** be used for long or slow operations, like
calling external programs or content, running unit tests...
Slow operations will hurt the whole Gerrit instance.
This can be used to implement custom rules that changes have to match to become
submittable. A more concrete example: the Prolog rules engine can be
implemented using this.
Gerrit calls the plugins once per change and caches the results. Although it is
possible to predict when this interface will be triggered, this should not be
considered as a feature. Plugins should only rely on the internal state of the
ChangeData, not on external values like date and time, remote content or
randomness.
Plugins are expected to support rules inheritance themselves, providing ways
to configure it and handling the logic behind it.
Please note that no inheritance is sometimes better than badly handled
inheritance: mis-communication and strange behaviors caused by inheritance
may and will confuse the users. Each plugins is responsible for handling the
project hierarchy and taking wise actions. Gerrit does not enforce it.
Once Gerrit has gathered every plugins' SubmitRecords, it stores them.
Plugins accept or reject a given change using `SubmitRecord.Status`.
If a change is ready to be submitted, `OK`. If it is not ready and requires
modifications, `NOT_READY`. Other statuses are available for particular cases.
A change can be submitted if all the plugins accept the change.
Plugins may also decide not to vote on a given change by returning an empty
Collection (ie: the plugin is not enabled for this repository), or to vote
several times (ie: one SubmitRecord per project in the hierarchy).
The results are handled as if multiple plugins voted for the change.
If a plugin decides not to vote, it's name will not be displayed in the UI and
it will not be recoded in the database.
.Gerrit's Pre-submit handling with three plugins
[width="50%",cols="^m,^m,^m,^m",frame="topbot",options="header"]
|=======================================================
| Plugin A | Plugin B | Plugin C | Final decision
| OK | OK | OK | OK
| OK | OK | / | OK
| OK | OK | RULE_ERROR | NOT_READY
| OK | NOT_READY | OK | NOT_READY
| NOT_READY | OK | OK | NOT_READY
|=======================================================
This makes composing plugins really easy.
- If a plugin places a veto on a change, it can't be submitted.
- If a plugin isn't enabled for a project (or isn't needed for this change),
it returns an empty collection.
- If all the plugins answer `OK`, the change can be submitted.
A more rare case, but worth documenting: if there are no installed plugins,
the labels will be compared to the rules defined in the project's config,
and the permission system will be used to allow or deny a submit request.
Some rules are defined internally to provide a common base ground (and sanity):
changes that are marked as WIP or that are closed (abandoned, merged) can't be merged.
[source, java]
----
import java.util.Collection;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRecord.Status;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.SubmitRule;
public class MyPluginRules implements SubmitRule {
public Collection<SubmitRecord> evaluate(ChangeData changeData) {
// Implement your submitability logic here
// Assuming we want to prevent this change from being submitted:
SubmitRecord record;
record.status = Status.NOT_READY;
return record;
}
}
----
Don't forget to register your class!
[source, java]
----
import com.google.gerrit.extensions.annotations.Exports;
import com.google.inject.AbstractModule;
public class MyPluginModule extends AbstractModule {
@Override
protected void configure() {
bind(SubmitRule.class).annotatedWith(Exports.named("myPlugin")).to(MyPluginRules.class);
}
}
----
== SEE ALSO

View File

@@ -178,6 +178,21 @@ status:: Current submit status.
labels:: This describes the state of each code review
<<label,label attribute>>, unless the status is RULE_ERROR.
requirements:: Each <<requirement>> describes what needs to be changed
in order for the change to be submittable.
[[requirement]]
== requirement
Information about a requirement (not met) in order to submit a change.
shortReason:: A short description of the requirement (a hint).
fullReason:: A longer and descriptive message explaining what needs to
be changed to meet the requirement.
label:: (Optional) The name of the linked label, if set by a pre-submit rule.
[[label]]
== label
Information about a code review label for a change.

View File

@@ -18,15 +18,20 @@ import com.google.gerrit.reviewdb.client.Account;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/** Describes the state required to submit a change. */
/** Describes the state and edits required to submit a change. */
public class SubmitRecord {
public static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
if (in == null) {
return Optional.empty();
public static boolean allRecordsOK(Collection<SubmitRecord> in) {
if (in == null || in.isEmpty()) {
// If the list is null or empty, it means that this Gerrit installation does not
// have any form of validation rules.
// Hence, the permission system should be used to determine if the change can be merged
// or not.
return true;
}
return in.stream().filter(r -> r.status == Status.OK).findFirst();
// The change can be submitted, unless at least one plugin prevents it.
return in.stream().noneMatch(r -> r.status != Status.OK);
}
public enum Status {
@@ -36,7 +41,7 @@ public class SubmitRecord {
/** The change is ready for submission. */
OK,
/** The change is missing a required label. */
/** Something is preventing this change from being submitted. */
NOT_READY,
/** The change has been closed. */
@@ -55,6 +60,7 @@ public class SubmitRecord {
public Status status;
public List<Label> labels;
public List<SubmitRequirement> requirements;
public String errorMessage;
public static class Label {
@@ -140,6 +146,14 @@ public class SubmitRecord {
delimiter = ", ";
}
}
sb.append("],[");
if (requirements != null) {
String delimiter = "";
for (SubmitRequirement requirement : requirements) {
sb.append(delimiter).append(requirement);
delimiter = ", ";
}
}
sb.append(']');
return sb.toString();
}
@@ -150,13 +164,14 @@ public class SubmitRecord {
SubmitRecord r = (SubmitRecord) o;
return Objects.equals(status, r.status)
&& Objects.equals(labels, r.labels)
&& Objects.equals(errorMessage, r.errorMessage);
&& Objects.equals(errorMessage, r.errorMessage)
&& Objects.equals(requirements, r.requirements);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(status, labels, errorMessage);
return Objects.hash(status, labels, errorMessage, requirements);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (C) 2018 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.common.data;
import static java.util.Objects.requireNonNull;
import com.google.gerrit.common.Nullable;
import java.util.Objects;
import java.util.Optional;
/** Describes a requirement to submit a change. */
public final class SubmitRequirement {
private final String shortReason;
private final String fullReason;
@Nullable private final String label;
public SubmitRequirement(String shortReason, String fullReason, @Nullable String label) {
this.shortReason = requireNonNull(shortReason);
this.fullReason = requireNonNull(fullReason);
this.label = label;
}
public String shortReason() {
return shortReason;
}
public String fullReason() {
return fullReason;
}
public Optional<String> label() {
return Optional.ofNullable(label);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof SubmitRequirement) {
SubmitRequirement that = (SubmitRequirement) o;
return Objects.equals(shortReason, that.shortReason)
&& Objects.equals(fullReason, that.fullReason)
&& Objects.equals(label, that.label);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(shortReason, fullReason, label);
}
@Override
public String toString() {
return "SubmitRequirement{"
+ "shortReason='"
+ shortReason
+ '\''
+ ", fullReason='"
+ fullReason
+ '\''
+ ", label='"
+ label
+ '\''
+ '}';
}
}

View File

@@ -157,7 +157,6 @@ public class BatchProgramModule extends FactoryModule {
install(new ExternalIdModule());
install(new GroupModule());
install(new NoteDbModule(cfg));
install(new PrologModule());
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
install(GroupIncludeCacheImpl.module());
@@ -169,7 +168,10 @@ public class BatchProgramModule extends FactoryModule {
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
factory(ProjectState.Factory.class);
// Submit rule evaluator
factory(SubmitRuleEvaluator.Factory.class);
install(new PrologModule());
bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));

View File

@@ -657,7 +657,7 @@ public class ChangeJson {
}
private boolean submittable(ChangeData cd) {
return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent();
return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
}
private List<SubmitRecord> submitRecords(ChangeData cd) {

View File

@@ -177,6 +177,7 @@ import com.google.gerrit.server.restapi.config.ConfigRestModule;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.PrologModule;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.ssh.SshAddressesModule;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.update.BatchUpdate;
@@ -391,6 +392,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), ActionVisitor.class);
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
DynamicSet.setOf(binder(), SubmitRule.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);

View File

@@ -14,6 +14,10 @@
package com.google.gerrit.server.data;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRecord.Label} that does not depend on
* Gerrit internal classes, to be serialized.
*/
public class SubmitLabelAttribute {
public String label;
public String status;

View File

@@ -16,7 +16,12 @@ package com.google.gerrit.server.data;
import java.util.List;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRecord} that does not depend on Gerrit
* internal classes, to be serialized.
*/
public class SubmitRecordAttribute {
public String status;
public List<SubmitLabelAttribute> labels;
public List<SubmitRequirementAttribute> requirements;
}

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2018 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.server.data;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRequirement} that does not depend on
* Gerrit internal classes, to be serialized
*/
public class SubmitRequirementAttribute {
public String shortReason;
public String fullReason;
public String label;
}

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -52,6 +53,7 @@ import com.google.gerrit.server.data.PatchSetCommentAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
import com.google.gerrit.server.data.SubmitLabelAttribute;
import com.google.gerrit.server.data.SubmitRecordAttribute;
import com.google.gerrit.server.data.SubmitRequirementAttribute;
import com.google.gerrit.server.data.TrackingIdAttribute;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchList;
@@ -229,6 +231,7 @@ public class EventFactory {
sa.status = submitRecord.status.name();
if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
addSubmitRecordLabels(submitRecord, sa);
addSubmitRecordRequirements(submitRecord, sa);
}
ca.submitRecords.add(sa);
}
@@ -253,6 +256,19 @@ public class EventFactory {
}
}
private void addSubmitRecordRequirements(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
if (submitRecord.requirements != null && !submitRecord.requirements.isEmpty()) {
sa.requirements = new ArrayList<>();
for (SubmitRequirement req : submitRecord.requirements) {
SubmitRequirementAttribute re = new SubmitRequirementAttribute();
re.shortReason = req.shortReason();
re.fullReason = req.fullReason();
re.label = req.label().orElse(null);
sa.requirements.add(re);
}
}
}
public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs) {
if (change == null || currentPs == null) {
return;

View File

@@ -291,7 +291,7 @@ public class MergeOp implements AutoCloseable {
throw new ResourceConflictException("missing current patch set for change " + cd.getId());
}
List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
if (SubmitRecord.findOkRecord(results).isPresent()) {
if (SubmitRecord.allRecordsOK(results)) {
// Rules supplied a valid solution.
return;
} else if (results.isEmpty()) {
@@ -303,6 +303,9 @@ public class MergeOp implements AutoCloseable {
for (SubmitRecord record : results) {
switch (record.status) {
case OK:
break;
case CLOSED:
throw new ResourceConflictException("change is closed");
@@ -313,7 +316,6 @@ public class MergeOp implements AutoCloseable {
throw new ResourceConflictException(describeLabels(cd, record.labels));
case FORCED:
case OK:
default:
throw new IllegalStateException(
String.format(

View File

@@ -36,7 +36,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.google.common.primitives.Longs;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.reviewdb.client.Account;
@@ -657,8 +659,15 @@ public class ChangeField {
Integer appliedBy;
}
static class StoredRequirement {
String shortReason;
String fullReason;
@Nullable String label;
}
SubmitRecord.Status status;
List<StoredLabel> labels;
List<StoredRequirement> requirements;
String errorMessage;
StoredSubmitRecord(SubmitRecord rec) {
@@ -674,6 +683,16 @@ public class ChangeField {
this.labels.add(sl);
}
}
if (rec.requirements != null) {
this.requirements = new ArrayList<>(rec.requirements.size());
for (SubmitRequirement requirement : rec.requirements) {
StoredRequirement sr = new StoredRequirement();
sr.shortReason = requirement.shortReason();
sr.fullReason = requirement.fullReason();
sr.label = requirement.label().orElse(null);
this.requirements.add(sr);
}
}
}
private SubmitRecord toSubmitRecord() {
@@ -690,6 +709,15 @@ public class ChangeField {
rec.labels.add(srl);
}
}
if (requirements != null) {
rec.requirements = new ArrayList<>(requirements.size());
for (StoredRequirement requirement : requirements) {
SubmitRequirement sr =
new SubmitRequirement(
requirement.shortReason, requirement.fullReason, requirement.label);
rec.requirements.add(sr);
}
}
return rec;
}
}

View File

@@ -728,6 +728,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
msg.append('\n');
}
}
// TODO(maximeg) We might want to list plugins that validated this submission.
}
}

View File

@@ -14,13 +14,14 @@
package com.google.gerrit.server.project;
@SuppressWarnings("serial")
public class RuleEvalException extends Exception {
private static final long serialVersionUID = 1L;
public RuleEvalException(String message) {
super(message);
}
RuleEvalException(String message, Throwable cause) {
public RuleEvalException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -14,38 +14,21 @@
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologEnvironment;
import com.google.gerrit.server.rules.StoredValues;
import com.google.gerrit.server.rules.PrologRule;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
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 java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,9 +38,31 @@ import org.slf4j.LoggerFactory;
*/
public class SubmitRuleEvaluator {
private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class);
private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
private final ProjectCache projectCache;
private final PrologRule prologRule;
private final DynamicSet<SubmitRule> submitRules;
private final SubmitRuleOptions opts;
public interface Factory {
/** Returns a new {@link SubmitRuleEvaluator} with the specified options */
SubmitRuleEvaluator create(SubmitRuleOptions options);
}
@Inject
private SubmitRuleEvaluator(
ProjectCache projectCache,
PrologRule prologRule,
DynamicSet<SubmitRule> submitRules,
@Assisted SubmitRuleOptions options) {
this.projectCache = projectCache;
this.prologRule = prologRule;
this.submitRules = submitRules;
this.opts = options;
}
public static List<SubmitRecord> defaultRuleError() {
return createRuleError(DEFAULT_MSG);
}
@@ -73,144 +78,26 @@ public class SubmitRuleEvaluator {
return SubmitTypeRecord.error(DEFAULT_MSG);
}
/**
* Exception thrown when the label term of a submit record unexpectedly didn't contain a user
* term.
*/
private static class UserTermExpected extends Exception {
private static final long serialVersionUID = 1L;
UserTermExpected(SubmitRecord.Label label) {
super(String.format("A label with the status %s must contain a user.", label.toString()));
}
}
public interface Factory {
SubmitRuleEvaluator create(ChangeData cd);
}
private final AccountCache accountCache;
private final Accounts accounts;
private final Emails emails;
private final ProjectCache projectCache;
private final ChangeData cd;
private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.builder();
private SubmitRuleOptions opts;
private Change change;
private PatchSet patchSet;
private boolean logErrors = true;
private long reductionsConsumed;
private ProjectState projectState;
private Term submitRule;
@Inject
SubmitRuleEvaluator(
AccountCache accountCache,
Accounts accounts,
Emails emails,
ProjectCache projectCache,
@Assisted ChangeData cd) {
this.accountCache = accountCache;
this.accounts = accounts;
this.emails = emails;
this.projectCache = projectCache;
this.cd = cd;
}
/**
* @return immutable snapshot of options configured so far. If neither {@link #getSubmitRule()}
* nor {@link #getSubmitType()} have been called yet, state within this instance is still
* mutable, so may change before evaluation. The instance's options are frozen at evaluation
* time.
*/
public SubmitRuleOptions getOptions() {
if (opts != null) {
return opts;
}
return optsBuilder.build();
}
public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) {
checkNotStarted();
if (opts != null) {
optsBuilder = opts.toBuilder();
} else {
optsBuilder = SubmitRuleOptions.builder();
}
return this;
}
/**
* @param ps patch set of the change to evaluate. If not set, the current patch set will be loaded
* from {@link #evaluate()} or {@link #getSubmitType}.
* @return this
*/
public SubmitRuleEvaluator setPatchSet(PatchSet ps) {
checkArgument(
ps.getId().getParentKey().equals(cd.getId()),
"Patch set %s does not match change %s",
ps.getId(),
cd.getId());
patchSet = ps;
return this;
}
/**
* @param allow whether to allow {@link #evaluate()} on closed changes.
* @return this
*/
public SubmitRuleEvaluator setAllowClosed(boolean allow) {
checkNotStarted();
optsBuilder.allowClosed(allow);
return this;
}
/**
* @param skip if true, submit filter will not be applied.
* @return this
*/
public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) {
checkNotStarted();
optsBuilder.skipFilters(skip);
return this;
}
/**
* @param rule custom rule to use, or null to use refs/meta/config:rules.pl.
* @return this
*/
public SubmitRuleEvaluator setRule(@Nullable String rule) {
checkNotStarted();
optsBuilder.rule(rule);
return this;
}
/**
* @param log whether to log error messages in addition to returning error records. If true, error
* record messages will be less descriptive.
*/
public SubmitRuleEvaluator setLogErrors(boolean log) {
logErrors = log;
return this;
}
/** @return Prolog reductions consumed during evaluation. */
public long getReductionsConsumed() {
return reductionsConsumed;
}
/**
* Evaluate the submit rules.
*
* @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
* errors.
* @param cd ChangeData to evaluate
*/
public List<SubmitRecord> evaluate() {
initOptions();
public List<SubmitRecord> evaluate(ChangeData cd) {
Change change;
ProjectState projectState;
try {
init();
change = cd.change();
if (change == null) {
throw new OrmException("Change not found");
}
projectState = projectCache.get(cd.project());
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (OrmException | NoSuchProjectException e) {
return ruleError("Error looking up change " + cd.getId(), e);
}
@@ -221,138 +108,16 @@ public class SubmitRuleEvaluator {
return Collections.singletonList(rec);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
} catch (RuleEvalException e) {
return ruleError(e.getMessage(), e);
}
if (results.isEmpty()) {
// This should never occur. A well written submit rule will always produce
// at least one result informing the caller of the labels that are
// required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions.
return ruleError(
String.format(
"Submit rule '%s' for change %s of %s has no solution.",
getSubmitRuleName(), cd.getId(), getProjectName()));
}
return resultsToSubmitRecord(getSubmitRule(), results);
}
/**
* Convert the results from Prolog Cafe's format to Gerrit's common format.
*
* <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
* using only that ok(P) record if it exists. This skips partial results that occur early in the
* output. Later after the loop the out collection is reversed to restore it to the original
* ordering.
*/
private List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
List<SubmitRecord> out = new ArrayList<>(results.size());
for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
Term submitRecord = results.get(resultIdx);
SubmitRecord rec = new SubmitRecord();
out.add(rec);
if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
return invalidResult(submitRule, submitRecord);
}
if ("ok".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.OK;
} else if ("not_ready".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.NOT_READY;
} else {
return invalidResult(submitRule, submitRecord);
}
// Unpack the one argument. This should also be a structure with one
// argument per label that needs to be reported on to the caller.
//
submitRecord = submitRecord.arg(0);
if (!(submitRecord instanceof StructureTerm)) {
return invalidResult(submitRule, submitRecord);
}
rec.labels = new ArrayList<>(submitRecord.arity());
for (Term state : ((StructureTerm) submitRecord).args()) {
if (!(state instanceof StructureTerm)
|| 2 != state.arity()
|| !"label".equals(state.name())) {
return invalidResult(submitRule, submitRecord);
}
SubmitRecord.Label lbl = new SubmitRecord.Label();
rec.labels.add(lbl);
lbl.label = state.arg(0).name();
Term status = state.arg(1);
try {
if ("ok".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.OK;
appliedBy(lbl, status);
} else if ("reject".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.REJECT;
appliedBy(lbl, status);
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
} else if ("may".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.MAY;
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
} else {
return invalidResult(submitRule, submitRecord);
}
} catch (UserTermExpected e) {
return invalidResult(submitRule, submitRecord, e.getMessage());
}
}
if (rec.status == SubmitRecord.Status.OK) {
break;
}
}
Collections.reverse(out);
return out;
}
private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
return ruleError(
String.format(
"Submit rule %s for change %s of %s output invalid result: %s%s",
rule,
cd.getId(),
getProjectName(),
record,
(reason == null ? "" : ". Reason: " + reason)));
}
private List<SubmitRecord> invalidResult(Term rule, Term record) {
return invalidResult(rule, record, null);
}
private List<SubmitRecord> ruleError(String err) {
return ruleError(err, null);
// We evaluate all the plugin-defined evaluators,
// and then we collect the results in one list.
return StreamSupport.stream(submitRules.spliterator(), false)
.map(s -> s.evaluate(cd, opts))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private List<SubmitRecord> ruleError(String err, Exception e) {
if (logErrors) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
@@ -367,73 +132,24 @@ public class SubmitRuleEvaluator {
* Evaluate the submit type rules to get the submit type.
*
* @return record from the evaluated rules.
* @param cd
*/
public SubmitTypeRecord getSubmitType() {
initOptions();
public SubmitTypeRecord getSubmitType(ChangeData cd) {
ProjectState projectState;
try {
init();
} catch (OrmException | NoSuchProjectException e) {
projectState = projectCache.get(cd.project());
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (NoSuchProjectException e) {
return typeError("Error looking up change " + cd.getId(), e);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_type",
"get_submit_type",
"locate_submit_type_filter",
"filter_submit_type_results");
} catch (RuleEvalException e) {
return typeError(e.getMessage(), e);
}
if (results.isEmpty()) {
// Should never occur for a well written rule
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " has no solution.");
}
Term typeTerm = results.get(0);
if (!(typeTerm instanceof SymbolTerm)) {
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " did not return a symbol.");
}
String typeName = ((SymbolTerm) typeTerm).name();
try {
return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
} catch (IllegalArgumentException e) {
return typeError(
"Submit type rule "
+ getSubmitRule()
+ " for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " output invalid result: "
+ typeName);
}
}
private SubmitTypeRecord typeError(String err) {
return typeError(err, null);
return prologRule.getSubmitType(cd, opts);
}
private SubmitTypeRecord typeError(String err, Exception e) {
if (logErrors) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
@@ -443,194 +159,4 @@ public class SubmitRuleEvaluator {
}
return SubmitTypeRecord.error(err);
}
private List<Term> evaluateImpl(
String userRuleLocatorName,
String userRuleWrapperName,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment env = getPrologEnvironment();
try {
Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
List<Term> results = new ArrayList<>();
try {
for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
results.add(template[1]);
}
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s", err.getMessage(), cd.getId().get(), getProjectName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s", sr, cd.getId().get(), getProjectName()),
err);
} finally {
reductionsConsumed = env.getReductions();
}
Term resultsTerm = toListTerm(results);
if (!opts.skipFilters()) {
resultsTerm =
runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
}
List<Term> r;
if (resultsTerm instanceof ListTerm) {
r = new ArrayList<>();
for (Term t = resultsTerm; t instanceof ListTerm; ) {
ListTerm l = (ListTerm) t;
r.add(l.car().dereference());
t = l.cdr().dereference();
}
} else {
r = Collections.emptyList();
}
submitRule = sr;
return r;
} finally {
env.close();
}
}
private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
PrologEnvironment env;
try {
if (opts.rule() == null) {
env = projectState.newPrologEnvironment();
} else {
env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
}
} catch (CompileException err) {
String msg;
if (opts.rule() == null) {
msg = String.format("Cannot load rules.pl for %s: %s", getProjectName(), err.getMessage());
} else {
msg = err.getMessage();
}
throw new RuleEvalException(msg, err);
}
env.set(StoredValues.ACCOUNTS, accounts);
env.set(StoredValues.ACCOUNT_CACHE, accountCache);
env.set(StoredValues.EMAILS, emails);
env.set(StoredValues.REVIEW_DB, cd.db());
env.set(StoredValues.CHANGE_DATA, cd);
env.set(StoredValues.PROJECT_STATE, projectState);
return env;
}
private Term runSubmitFilters(
Term results,
PrologEnvironment env,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment childEnv = env;
for (ProjectState parentState : projectState.parents()) {
PrologEnvironment parentEnv;
try {
parentEnv = parentState.newPrologEnvironment();
} catch (CompileException err) {
throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
}
parentEnv.copyStoredValues(childEnv);
Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
try {
Term[] template =
parentEnv.once(
"gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
results = template[2];
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), parentState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
filterRule, cd.getId().get(), parentState.getName()),
err);
} finally {
reductionsConsumed += env.getReductions();
}
childEnv = parentEnv;
}
return results;
}
private static Term toListTerm(List<Term> terms) {
Term list = Prolog.Nil;
for (int i = terms.size() - 1; i >= 0; i--) {
list = new ListTerm(terms.get(i), list);
}
return list;
}
private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
if (status instanceof StructureTerm && status.arity() == 1) {
Term who = status.arg(0);
if (isUser(who)) {
label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
} else {
throw new UserTermExpected(label);
}
}
}
private static boolean isUser(Term who) {
return who instanceof StructureTerm
&& who.arity() == 1
&& who.name().equals("user")
&& who.arg(0) instanceof IntegerTerm;
}
public Term getSubmitRule() {
checkState(submitRule != null, "getSubmitRule() invalid before evaluation");
return submitRule;
}
public String getSubmitRuleName() {
return submitRule != null ? submitRule.toString() : "<unknown rule>";
}
private void checkNotStarted() {
checkState(opts == null, "cannot set options after starting evaluation");
}
private void initOptions() {
if (opts == null) {
opts = optsBuilder.build();
optsBuilder = null;
}
}
private void init() throws OrmException, NoSuchProjectException {
if (change == null) {
change = cd.change();
if (change == null) {
throw new OrmException("No change found");
}
}
if (projectState == null) {
projectState = projectCache.get(change.getProject());
if (projectState == null) {
throw new NoSuchProjectException(change.getProject());
}
}
if (patchSet == null) {
patchSet = cd.currentPatchSet();
if (patchSet == null) {
throw new OrmException("No patch set found");
}
}
}
private String getProjectName() {
return projectState.getName();
}
}

View File

@@ -25,17 +25,28 @@ import com.google.gerrit.common.Nullable;
*/
@AutoValue
public abstract class SubmitRuleOptions {
private static final SubmitRuleOptions defaults =
new AutoValue_SubmitRuleOptions.Builder()
.allowClosed(false)
.skipFilters(false)
.logErrors(true)
.rule(null)
.build();
public static SubmitRuleOptions defaults() {
return defaults;
}
public static Builder builder() {
return new AutoValue_SubmitRuleOptions.Builder()
.allowClosed(false)
.skipFilters(false)
.rule(null);
return defaults.toBuilder();
}
public abstract boolean allowClosed();
public abstract boolean skipFilters();
public abstract boolean logErrors();
@Nullable
public abstract String rule();
@@ -49,6 +60,8 @@ public abstract class SubmitRuleOptions {
public abstract SubmitRuleOptions.Builder rule(@Nullable String rule);
public abstract SubmitRuleOptions.Builder logErrors(boolean logErrors);
public abstract SubmitRuleOptions build();
}
}

View File

@@ -943,7 +943,7 @@ public class ChangeData {
if (!lazyLoad) {
return Collections.emptyList();
}
records = submitRuleEvaluatorFactory.create(this).setOptions(options).evaluate();
records = submitRuleEvaluatorFactory.create(options).evaluate(this);
submitRecords.put(options, records);
}
return records;
@@ -960,7 +960,8 @@ public class ChangeData {
public SubmitTypeRecord submitTypeRecord() {
if (submitTypeRecord == null) {
submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
submitTypeRecord =
submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults()).getSubmitType(this);
}
return submitTypeRecord;
}

View File

@@ -31,6 +31,7 @@ import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -249,8 +250,8 @@ public class OutputStreamQuery {
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(
c, submitRuleEvaluatorFactory.create(d).setAllowClosed(true).evaluate());
SubmitRuleOptions options = SubmitRuleOptions.builder().allowClosed(true).build();
eventFactory.addSubmitRecords(c, submitRuleEvaluatorFactory.create(options).evaluate(d));
}
if (includeCommitMessage) {

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -70,7 +71,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
private final Provider<ReviewDb> db;
private final ChangeIndexer indexer;
private final MergeabilityCache cache;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
private final SubmitRuleEvaluator submitRuleEvaluator;
@Inject
Mergeable(
@@ -89,7 +90,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
this.db = db;
this.indexer = indexer;
this.cache = cache;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
}
public void setOtherBranches(boolean otherBranches) {
@@ -112,7 +113,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
}
ChangeData cd = changeDataFactory.create(db.get(), resource.getNotes());
result.submitType = getSubmitType(cd, ps);
result.submitType = getSubmitType(cd);
try (Repository git = gitManager.openRepository(change.getProject())) {
ObjectId commit = toId(ps);
@@ -144,9 +145,8 @@ public class Mergeable implements RestReadView<RevisionResource> {
return result;
}
private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
SubmitTypeRecord rec =
submitRuleEvaluatorFactory.create(cd).setPatchSet(patchSet).getSubmitType();
private SubmitType getSubmitType(ChangeData cd) throws OrmException {
SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd);
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new OrmException("Submit type rule failed: " + rec);
}

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -49,7 +50,7 @@ public class ReviewerJson {
private final ChangeData.Factory changeDataFactory;
private final ApprovalsUtil approvalsUtil;
private final AccountLoader.Factory accountLoaderFactory;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
private final SubmitRuleEvaluator submitRuleEvaluator;
@Inject
ReviewerJson(
@@ -64,7 +65,7 @@ public class ReviewerJson {
this.changeDataFactory = changeDataFactory;
this.approvalsUtil = approvalsUtil;
this.accountLoaderFactory = accountLoaderFactory;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
}
public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
@@ -124,7 +125,7 @@ public class ReviewerJson {
// do not exist in the DB.
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
for (SubmitRecord rec : submitRuleEvaluatorFactory.create(cd).evaluate()) {
for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
if (rec.labels == null) {
continue;
}

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gwtorm.server.OrmException;
@@ -70,24 +71,22 @@ public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubm
throw new AuthException("project rules are disabled");
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
List<SubmitRecord> records =
evaluator
.setPatchSet(rsrc.getPatchSet())
.setLogErrors(false)
.setSkipSubmitFilters(input.filters == Filters.SKIP)
.setRule(input.rule)
.evaluate();
SubmitRuleOptions opts =
SubmitRuleOptions.builder()
.skipFilters(input.filters == Filters.SKIP)
.rule(input.rule)
.logErrors(false)
.build();
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
List<SubmitRecord> records = submitRuleEvaluatorFactory.create(opts).evaluate(cd);
List<Record> out = Lists.newArrayListWithCapacity(records.size());
AccountLoader accounts = accountInfoFactory.create(true);
for (SubmitRecord r : records) {
out.add(new Record(r, accounts));
}
if (!out.isEmpty()) {
out.get(0).prologReductionCount = evaluator.getReductionsConsumed();
}
accounts.fill();
return out;
}
@@ -100,7 +99,6 @@ public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubm
Map<String, None> need;
Map<String, AccountInfo> may;
Map<String, None> impossible;
Long prologReductionCount;
Record(SubmitRecord r, AccountLoader accounts) {
this.status = r.status;

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gwtorm.server.OrmException;
@@ -64,19 +65,20 @@ public class TestSubmitType implements RestModifyView<RevisionResource, TestSubm
throw new AuthException("project rules are disabled");
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
SubmitTypeRecord rec =
evaluator
.setPatchSet(rsrc.getPatchSet())
.setLogErrors(false)
.setSkipSubmitFilters(input.filters == Filters.SKIP)
.setRule(input.rule)
.getSubmitType();
SubmitRuleOptions opts =
SubmitRuleOptions.builder()
.logErrors(false)
.skipFilters(input.filters == Filters.SKIP)
.rule(input.rule)
.build();
SubmitRuleEvaluator evaluator = submitRuleEvaluatorFactory.create(opts);
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
SubmitTypeRecord rec = evaluator.getSubmitType(cd);
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new BadRequestException(
String.format("rule %s produced invalid result: %s", evaluator.getSubmitRuleName(), rec));
throw new BadRequestException(String.format("rule produced invalid result: %s", rec));
}
return rec.type;

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.rules;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -22,6 +23,9 @@ public class PrologModule extends FactoryModule {
protected void configure() {
install(new EnvironmentModule());
bind(PrologEnvironment.Args.class);
factory(PrologRuleEvaluator.Factory.class);
bind(SubmitRule.class).annotatedWith(Exports.named("PrologRule")).to(PrologRule.class);
}
static class EnvironmentModule extends FactoryModule {

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2018 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.server.rules;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologRuleEvaluator.Factory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
@Singleton
public class PrologRule implements SubmitRule {
private final Factory factory;
@Inject
private PrologRule(PrologRuleEvaluator.Factory factory) {
this.factory = factory;
}
@Override
public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
return getEvaluator(cd, opts).evaluate();
}
private PrologRuleEvaluator getEvaluator(ChangeData cd, SubmitRuleOptions opts) {
return factory.create(cd, opts);
}
public SubmitTypeRecord getSubmitType(ChangeData cd, SubmitRuleOptions opts) {
return getEvaluator(cd, opts).getSubmitType();
}
}

View File

@@ -0,0 +1,519 @@
// Copyright (C) 2012 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.server.rules;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.createRuleError;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RuleEvalException;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
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 java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
* the results through rules found in the parent projects, all the way up to All-Projects.
*/
public class PrologRuleEvaluator {
private static final Logger log = LoggerFactory.getLogger(PrologRuleEvaluator.class);
public interface Factory {
/** Returns a new {@link PrologRuleEvaluator} with the specified options */
PrologRuleEvaluator create(ChangeData cd, SubmitRuleOptions options);
}
/**
* Exception thrown when the label term of a submit record unexpectedly didn't contain a user
* term.
*/
private static class UserTermExpected extends Exception {
private static final long serialVersionUID = 1L;
UserTermExpected(SubmitRecord.Label label) {
super(String.format("A label with the status %s must contain a user.", label.toString()));
}
}
private final AccountCache accountCache;
private final Accounts accounts;
private final Emails emails;
private final ChangeData cd;
private final ProjectState projectState;
private final SubmitRuleOptions opts;
private Term submitRule;
@AssistedInject
private PrologRuleEvaluator(
AccountCache accountCache,
Accounts accounts,
Emails emails,
ProjectCache projectCache,
@Assisted ChangeData cd,
@Assisted SubmitRuleOptions options) {
this.accountCache = accountCache;
this.accounts = accounts;
this.emails = emails;
this.cd = cd;
this.opts = options;
this.projectState = projectCache.get(cd.project());
}
private static Term toListTerm(List<Term> terms) {
Term list = Prolog.Nil;
for (int i = terms.size() - 1; i >= 0; i--) {
list = new ListTerm(terms.get(i), list);
}
return list;
}
private static boolean isUser(Term who) {
return who instanceof StructureTerm
&& who.arity() == 1
&& who.name().equals("user")
&& who.arg(0) instanceof IntegerTerm;
}
private Term getSubmitRule() {
return submitRule;
}
/**
* Evaluate the submit rules.
*
* @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
* errors.
*/
public Collection<SubmitRecord> evaluate() {
Change change;
try {
change = cd.change();
if (change == null) {
throw new OrmException("No change found");
}
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (OrmException | NoSuchProjectException e) {
return ruleError("Error looking up change " + cd.getId(), e);
}
if (!opts.allowClosed() && change.getStatus().isClosed()) {
SubmitRecord rec = new SubmitRecord();
rec.status = SubmitRecord.Status.CLOSED;
return Collections.singletonList(rec);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
} catch (RuleEvalException e) {
return ruleError(e.getMessage(), e);
}
if (results.isEmpty()) {
// This should never occur. A well written submit rule will always produce
// at least one result informing the caller of the labels that are
// required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions.
return ruleError(
String.format(
"Submit rule '%s' for change %s of %s has no solution.",
getSubmitRuleName(), cd.getId(), projectState.getName()));
}
return resultsToSubmitRecord(getSubmitRule(), results);
}
private String getSubmitRuleName() {
return submitRule == null ? "<unknown>" : submitRule.name();
}
/**
* Convert the results from Prolog Cafe's format to Gerrit's common format.
*
* <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
* using only that ok(P) record if it exists. This skips partial results that occur early in the
* output. Later after the loop the out collection is reversed to restore it to the original
* ordering.
*/
public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
boolean foundOk = false;
List<SubmitRecord> out = new ArrayList<>(results.size());
for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
Term submitRecord = results.get(resultIdx);
SubmitRecord rec = new SubmitRecord();
out.add(rec);
if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
return invalidResult(submitRule, submitRecord);
}
if ("ok".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.OK;
} else if ("not_ready".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.NOT_READY;
} else {
return invalidResult(submitRule, submitRecord);
}
// Unpack the one argument. This should also be a structure with one
// argument per label that needs to be reported on to the caller.
//
submitRecord = submitRecord.arg(0);
if (!(submitRecord instanceof StructureTerm)) {
return invalidResult(submitRule, submitRecord);
}
rec.labels = new ArrayList<>(submitRecord.arity());
for (Term state : ((StructureTerm) submitRecord).args()) {
if (!(state instanceof StructureTerm)
|| 2 != state.arity()
|| !"label".equals(state.name())) {
return invalidResult(submitRule, submitRecord);
}
SubmitRecord.Label lbl = new SubmitRecord.Label();
rec.labels.add(lbl);
lbl.label = state.arg(0).name();
Term status = state.arg(1);
try {
if ("ok".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.OK;
appliedBy(lbl, status);
} else if ("reject".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.REJECT;
appliedBy(lbl, status);
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
} else if ("may".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.MAY;
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
} else {
return invalidResult(submitRule, submitRecord);
}
} catch (UserTermExpected e) {
return invalidResult(submitRule, submitRecord, e.getMessage());
}
}
if (rec.status == SubmitRecord.Status.OK) {
foundOk = true;
break;
}
}
Collections.reverse(out);
// This transformation is required to adapt Prolog's behavior to the way Gerrit handles
// SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
// When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
// the change to be submittable when at least one result is OK.
if (foundOk) {
for (SubmitRecord record : out) {
record.status = SubmitRecord.Status.OK;
}
}
return out;
}
private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
return ruleError(
String.format(
"Submit rule %s for change %s of %s output invalid result: %s%s",
rule,
cd.getId(),
cd.project().get(),
record,
(reason == null ? "" : ". Reason: " + reason)));
}
private List<SubmitRecord> invalidResult(Term rule, Term record) {
return invalidResult(rule, record, null);
}
private List<SubmitRecord> ruleError(String err) {
return ruleError(err, null);
}
private List<SubmitRecord> ruleError(String err, Exception e) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
log.error(err, e);
}
return defaultRuleError();
}
return createRuleError(err);
}
/**
* Evaluate the submit type rules to get the submit type.
*
* @return record from the evaluated rules.
*/
public SubmitTypeRecord getSubmitType() {
try {
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (NoSuchProjectException e) {
return typeError("Error looking up change " + cd.getId(), e);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_type",
"get_submit_type",
"locate_submit_type_filter",
"filter_submit_type_results");
} catch (RuleEvalException e) {
return typeError(e.getMessage(), e);
}
if (results.isEmpty()) {
// Should never occur for a well written rule
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " has no solution.");
}
Term typeTerm = results.get(0);
if (!(typeTerm instanceof SymbolTerm)) {
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " did not return a symbol.");
}
String typeName = typeTerm.name();
try {
return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
} catch (IllegalArgumentException e) {
return typeError(
"Submit type rule "
+ getSubmitRule()
+ " for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " output invalid result: "
+ typeName);
}
}
private SubmitTypeRecord typeError(String err) {
return typeError(err, null);
}
private SubmitTypeRecord typeError(String err, Exception e) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
log.error(err, e);
}
return defaultTypeError();
}
return SubmitTypeRecord.error(err);
}
private List<Term> evaluateImpl(
String userRuleLocatorName,
String userRuleWrapperName,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment env = getPrologEnvironment();
try {
Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
List<Term> results = new ArrayList<>();
try {
for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
results.add(template[1]);
}
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), projectState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
sr, cd.getId().get(), projectState.getName()),
err);
}
Term resultsTerm = toListTerm(results);
if (!opts.skipFilters()) {
resultsTerm =
runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
}
List<Term> r;
if (resultsTerm instanceof ListTerm) {
r = new ArrayList<>();
for (Term t = resultsTerm; t instanceof ListTerm; ) {
ListTerm l = (ListTerm) t;
r.add(l.car().dereference());
t = l.cdr().dereference();
}
} else {
r = Collections.emptyList();
}
submitRule = sr;
return r;
} finally {
env.close();
}
}
private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
PrologEnvironment env;
try {
if (opts.rule() == null) {
env = projectState.newPrologEnvironment();
} else {
env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
}
} catch (CompileException err) {
String msg;
if (opts.rule() == null) {
msg =
String.format(
"Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
} else {
msg = err.getMessage();
}
throw new RuleEvalException(msg, err);
}
env.set(StoredValues.ACCOUNTS, accounts);
env.set(StoredValues.ACCOUNT_CACHE, accountCache);
env.set(StoredValues.EMAILS, emails);
env.set(StoredValues.REVIEW_DB, cd.db());
env.set(StoredValues.CHANGE_DATA, cd);
env.set(StoredValues.PROJECT_STATE, projectState);
return env;
}
private Term runSubmitFilters(
Term results,
PrologEnvironment env,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment childEnv = env;
ChangeData cd = env.get(StoredValues.CHANGE_DATA);
ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
for (ProjectState parentState : projectState.parents()) {
PrologEnvironment parentEnv;
try {
parentEnv = parentState.newPrologEnvironment();
} catch (CompileException err) {
throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
}
parentEnv.copyStoredValues(childEnv);
Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
try {
Term[] template =
parentEnv.once(
"gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
results = template[2];
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), parentState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
filterRule, cd.getId().get(), parentState.getName()),
err);
}
childEnv = parentEnv;
}
return results;
}
private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
if (status instanceof StructureTerm && status.arity() == 1) {
Term who = status.arg(0);
if (isUser(who)) {
label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
} else {
throw new UserTermExpected(label);
}
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2018 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.server.rules;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import java.util.Collection;
/**
* Allows plugins to decide whether a change is ready to be submitted or not.
*
* <p>For a given {@link ChangeData}, each plugin is called and returns a {@link Collection} of
* {@link SubmitRecord}. This collection can be empty, or contain one or several values.
*
* <p>A Change can only be submitted if all the plugins give their consent.
*
* <p>Each {@link SubmitRecord} represents a decision made by the plugin. If the plugin rejects a
* change, it should hold valuable informations to help the end user understand and correct the
* blocking points.
*
* <p>It should be noted that each plugin can handle rules inheritance.
*
* <p>This interface should be used to write pre-submit validation rules. This includes both simple
* checks, coded in Java, and more complex fully fledged expression evaluators (think: Prolog,
* JavaCC, or even JavaScript rules).
*/
@ExtensionPoint
public interface SubmitRule {
/** Returns a {@link Collection} of {@link SubmitRecord} status for the change. */
Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options);
}

View File

@@ -0,0 +1,10 @@
load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
acceptance_tests(
srcs = glob(["*IT.java"]),
group = "server_rules",
labels = ["server"],
deps = [
"@prolog_runtime//jar",
],
)

View File

@@ -0,0 +1,160 @@
// Copyright (C) 2018 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.acceptance.server.rules;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologRuleEvaluator;
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.Term;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
public class PrologRuleEvaluatorIT extends AbstractDaemonTest {
@Inject private PrologRuleEvaluator.Factory evaluatorFactory;
@Test
public void convertsPrologToSubmitRecord() {
PrologRuleEvaluator evaluator = makeEvaluator();
StructureTerm verifiedLabel = makeLabel("Verified", "may");
StructureTerm labels = new StructureTerm("label", verifiedLabel);
List<Term> terms = ImmutableList.of(makeTerm("ok", labels));
Collection<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
assertThat(records).hasSize(1);
}
/**
* The Prolog behavior is everything but intuitive. Several submit_rules can be defined, and each
* will provide a different SubmitRecord answer when queried. The current implementation stops
* parsing the Prolog terms into SubmitRecord objects once it finds an OK record. This might lead
* to tangling results, as reproduced by this test.
*
* <p>Let's consider this rules.pl file (equivalent to the code in this test)
*
* <pre>{@code
* submit_rule(submit(R)) :-
* gerrit:uploader(U),
* R = label('Verified', reject(U)).
*
* submit_rule(submit(CR, V)) :-
* gerrit:uploader(U),
* V = label('Code-Review', ok(U)).
*
* submit_rule(submit(R)) :-
* gerrit:uploader(U),
* R = label('Any-Label-Name', reject(U)).
* }</pre>
*
* The first submit_rule always fails because the Verified label is rejected.
*
* <p>The second submit_rule is always valid, and provides two labels: OK and Code-Review.
*
* <p>The third submit_rule always fails because the Any-Label-Name label is rejected.
*
* <p>In this case, the last two SubmitRecords are used, the first one is discarded.
*/
@Test
public void abortsEarlyWithOkayRecord() {
PrologRuleEvaluator evaluator = makeEvaluator();
SubmitRecord.Label submitRecordLabel1 = new SubmitRecord.Label();
submitRecordLabel1.label = "Verified";
submitRecordLabel1.status = SubmitRecord.Label.Status.REJECT;
submitRecordLabel1.appliedBy = admin.id;
SubmitRecord.Label submitRecordLabel2 = new SubmitRecord.Label();
submitRecordLabel2.label = "Code-Review";
submitRecordLabel2.status = SubmitRecord.Label.Status.OK;
submitRecordLabel2.appliedBy = admin.id;
SubmitRecord.Label submitRecordLabel3 = new SubmitRecord.Label();
submitRecordLabel3.label = "Any-Label-Name";
submitRecordLabel3.status = SubmitRecord.Label.Status.REJECT;
submitRecordLabel3.appliedBy = user.id;
List<Term> terms = new ArrayList<>();
StructureTerm label1 = makeLabel(submitRecordLabel1.label, "reject", admin);
StructureTerm label2 = makeLabel(submitRecordLabel2.label, "ok", admin);
StructureTerm label3 = makeLabel(submitRecordLabel3.label, "reject", user);
terms.add(makeTerm("not_ready", makeLabels(label1)));
terms.add(makeTerm("ok", makeLabels(label2)));
terms.add(makeTerm("not_ready", makeLabels(label3)));
// When
List<SubmitRecord> records = evaluator.resultsToSubmitRecord(null, terms);
// assert that
SubmitRecord record1Expected = new SubmitRecord();
record1Expected.status = SubmitRecord.Status.OK;
record1Expected.labels = new ArrayList<>();
record1Expected.labels.add(submitRecordLabel2);
SubmitRecord record2Expected = new SubmitRecord();
record2Expected.status = SubmitRecord.Status.OK;
record2Expected.labels = new ArrayList<>();
record2Expected.labels.add(submitRecordLabel3);
assertThat(records).hasSize(2);
assertThat(records.get(0)).isEqualTo(record1Expected);
assertThat(records.get(1)).isEqualTo(record2Expected);
}
private static Term makeTerm(String status, StructureTerm labels) {
return new StructureTerm(status, labels);
}
private static StructureTerm makeLabel(String name, String status) {
return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status));
}
private static StructureTerm makeLabel(String name, String status, TestAccount account) {
StructureTerm user = new StructureTerm("user", new IntegerTerm(account.id.get()));
return new StructureTerm("label", new StructureTerm(name), new StructureTerm(status, user));
}
private static StructureTerm makeLabels(StructureTerm... labels) {
return new StructureTerm("label", labels);
}
private ChangeData makeChangeData() {
ChangeData cd = ChangeData.createForTest(project, new Change.Id(1), 1);
cd.setChange(TestChanges.newChange(project, admin.id));
return cd;
}
private PrologRuleEvaluator makeEvaluator() {
return evaluatorFactory.create(makeChangeData(), SubmitRuleOptions.defaults());
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (C) 2018 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.common.data;
import static com.google.common.truth.Truth.assertThat;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.Test;
public class SubmitRecordTest {
private static SubmitRecord OK_RECORD;
private static SubmitRecord NOT_READY_RECORD;
static {
OK_RECORD = new SubmitRecord();
OK_RECORD.status = SubmitRecord.Status.OK;
NOT_READY_RECORD = new SubmitRecord();
NOT_READY_RECORD.status = SubmitRecord.Status.NOT_READY;
}
@Test
public void okIfAllOkay() {
Collection<SubmitRecord> submitRecords = new ArrayList<>();
submitRecords.add(OK_RECORD);
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
}
@Test
public void okWhenEmpty() {
Collection<SubmitRecord> submitRecords = new ArrayList<>();
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isTrue();
}
@Test
public void emptyResultIfInvalid() {
Collection<SubmitRecord> submitRecords = new ArrayList<>();
submitRecords.add(NOT_READY_RECORD);
submitRecords.add(OK_RECORD);
assertThat(SubmitRecord.allRecordsOK(submitRecords)).isFalse();
}
}

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Table;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.ReviewerSet;
@@ -30,6 +31,7 @@ import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.testing.GerritBaseTests;
import com.google.gerrit.testing.TestTimeUtil;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.After;
@@ -80,11 +82,32 @@ public class ChangeFieldTest extends GerritBaseTests {
@Test
public void storedSubmitRecords() {
assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
assertStoredRecordRoundTrip(
SubmitRecord r =
record(
SubmitRecord.Status.OK,
label(SubmitRecord.Label.Status.MAY, "Label-1", null),
label(SubmitRecord.Label.Status.OK, "Label-2", 1)));
label(SubmitRecord.Label.Status.OK, "Label-2", 1));
assertStoredRecordRoundTrip(r);
}
@Test
public void storedSubmitRecordsWithRequirements() {
SubmitRecord r =
record(
SubmitRecord.Status.OK,
label(SubmitRecord.Label.Status.MAY, "Label-1", null),
label(SubmitRecord.Label.Status.OK, "Label-2", 1));
SubmitRequirement sr =
new SubmitRequirement(
"short reason",
"Full reason can be a long string with special symbols like < > \\ / ; :",
null);
r.requirements = Collections.singletonList(sr);
assertStoredRecordRoundTrip(r);
}
private static SubmitRecord record(SubmitRecord.Status status, SubmitRecord.Label... labels) {