Merge "GetCheckerIT: Add tests for newly added checker fields"
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
index 8bf6709..67a29f8 100644
--- a/java/com/google/gerrit/plugins/checks/Checker.java
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -15,17 +15,36 @@
package com.google.gerrit.plugins.checks;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.plugins.checks.api.BlockingCondition;
import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.ProjectPredicate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
import java.sql.Timestamp;
+import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
/** Definition of a checker. */
@AutoValue
public abstract class Checker {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Returns the UUID of the checker.
@@ -126,6 +145,109 @@
return builder().setUuid(uuid);
}
+ public boolean isCheckerRelevant(ChangeData cd, ChangeQueryBuilder changeQueryBuilder)
+ throws OrmException {
+ if (!getQuery().isPresent()) {
+ return cd.change().isNew();
+ }
+
+ Predicate<ChangeData> predicate;
+ try {
+ predicate = createQueryPredicate(changeQueryBuilder);
+ } catch (ConfigInvalidException e) {
+ logger.atWarning().withCause(e).log("skipping invalid query for checker %s", getUuid());
+ return false;
+ }
+
+ return predicate.asMatchable().match(cd);
+ }
+
+ public List<ChangeData> queryMatchingChanges(
+ RetryHelper retryHelper,
+ ChangeQueryBuilder changeQueryBuilder,
+ Provider<ChangeQueryProcessor> changeQueryProcessorProvider)
+ throws ConfigInvalidException, OrmException {
+ return executeIndexQueryWithRetry(
+ retryHelper, changeQueryProcessorProvider, createQueryPredicate(changeQueryBuilder));
+ }
+
+ private Predicate<ChangeData> createQueryPredicate(ChangeQueryBuilder changeQueryBuilder)
+ throws ConfigInvalidException {
+ Predicate<ChangeData> predicate = new ProjectPredicate(getRepository().get());
+
+ if (getQuery().isPresent()) {
+ String query = getQuery().get();
+ Predicate<ChangeData> predicateForQuery;
+ try {
+ predicateForQuery = changeQueryBuilder.parse(query);
+ } catch (QueryParseException e) {
+ throw new ConfigInvalidException(
+ String.format("change query of checker %s is invalid: %s", getUuid(), query), e);
+ }
+
+ if (!predicateForQuery.isMatchable()) {
+ // Assuming nobody modified the query behind Gerrit's back, this is programmer error:
+ // CheckerQuery should not be able to produce non-matchable queries.
+ throw new ConfigInvalidException(
+ String.format("change query of checker %s is non-matchable: %s", getUuid(), query));
+ }
+
+ predicate = Predicate.and(predicate, predicateForQuery);
+ }
+
+ if (!hasStatusPredicate(predicate)) {
+ predicate = Predicate.and(ChangeStatusPredicate.open(), predicate);
+ }
+
+ return predicate;
+ }
+
+ private static boolean hasStatusPredicate(Predicate<ChangeData> predicate) {
+ if (predicate instanceof IndexPredicate) {
+ return ((IndexPredicate<ChangeData>) predicate)
+ .getField()
+ .getName()
+ .equals(ChangeField.STATUS.getName());
+ }
+ return predicate.getChildren().stream().anyMatch(Checker::hasStatusPredicate);
+ }
+
+ // TODO(ekempin): Retrying the query should be done by ChangeQueryProcessor.
+ private List<ChangeData> executeIndexQueryWithRetry(
+ RetryHelper retryHelper,
+ Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
+ Predicate<ChangeData> predicate)
+ throws OrmException {
+ try {
+ return retryHelper.execute(
+ ActionType.INDEX_QUERY,
+ () -> changeQueryProcessorProvider.get().query(predicate).entities(),
+ OrmException.class::isInstance);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, OrmException.class);
+ throw new OrmException(e);
+ }
+ }
+
+ /**
+ * Checks whether a {@link Checker} is required for submission or not.
+ *
+ * @return true if the {@link Checker} required for submission.
+ */
+ public boolean isRequired() {
+ ImmutableSet<BlockingCondition> blockingConditions = getBlockingConditions();
+ if (blockingConditions.isEmpty()) {
+ return false;
+ } else if (blockingConditions.size() > 1
+ || !blockingConditions.contains(BlockingCondition.STATE_NOT_PASSING)) {
+ // When a new blocking condition is introduced, this needs to be adjusted to respect that.
+ String errorMessage = String.format("illegal blocking conditions %s", blockingConditions);
+ throw new IllegalStateException(errorMessage);
+ }
+ return true;
+ }
+
/** A builder for an {@link Checker}. */
@AutoValue.Builder
public abstract static class Builder {
diff --git a/java/com/google/gerrit/plugins/checks/Checks.java b/java/com/google/gerrit/plugins/checks/Checks.java
index 747d81b..7d6cd03 100644
--- a/java/com/google/gerrit/plugins/checks/Checks.java
+++ b/java/com/google/gerrit/plugins/checks/Checks.java
@@ -14,10 +14,13 @@
package com.google.gerrit.plugins.checks;
+import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
import java.util.List;
import java.util.Optional;
@@ -37,15 +40,51 @@
*
* @param projectName the name of the project
* @param patchSetId the ID of the patch set
+ * @param options options for getting checks.
* @return the checks, {@link Optional#empty()} if no checks with the given UUID exists
* @throws OrmException if the checks couldn't be retrieved from the storage
*/
- ImmutableList<Check> getChecks(Project.NameKey projectName, PatchSet.Id patchSetId)
- throws OrmException;
+ ImmutableList<Check> getChecks(
+ Project.NameKey projectName, PatchSet.Id patchSetId, GetCheckOptions options)
+ throws OrmException, IOException;
/**
* Returns a {@link Optional} holding a single check. {@code Optional.empty()} if the check does
* not exist.
+ *
+ * @param checkKey the key of the target check.
+ * @param options options for getting a check.
+ * @return the target check if it exists. A backfilled check will be returned if {@link
+ * GetCheckOptions#backfillChecks()} is true.
+ * @throws OrmException if the check couldn't be retrieved from the storage
+ * @throws IOException if the check couldn't be retrieved from the storage
*/
- Optional<Check> getCheck(CheckKey checkKey) throws OrmException;
+ Optional<Check> getCheck(CheckKey checkKey, GetCheckOptions options)
+ throws OrmException, IOException;
+
+ /**
+ * Returns the combined check state of a given patch set.
+ *
+ * @param projectName the name of the project.
+ * @param patchSetId the ID of the patch set
+ * @return the {@link CombinedCheckState} of the current patch set.
+ * @throws IOException if failed to get the {@link CombinedCheckState}.
+ * @throws OrmException if failed to get the {@link CombinedCheckState}.
+ */
+ CombinedCheckState getCombinedCheckState(Project.NameKey projectName, PatchSet.Id patchSetId)
+ throws IOException, OrmException;
+
+ @AutoValue
+ abstract class GetCheckOptions {
+ public static GetCheckOptions defaults() {
+ return new AutoValue_Checks_GetCheckOptions(false);
+ }
+
+ public static GetCheckOptions withBackfilling() {
+ return new AutoValue_Checks_GetCheckOptions(true);
+ }
+
+ /** Backfills checks for relevant checkers with default when they don't exist yet. */
+ public abstract boolean backfillChecks();
+ }
}
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
index ba535e2..987a227 100644
--- a/java/com/google/gerrit/plugins/checks/Module.java
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -21,10 +21,15 @@
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.plugins.checks.api.ApiModule;
+import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory;
+import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory.GetChangeOptions;
import com.google.gerrit.plugins.checks.db.NoteDbCheckersModule;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.restapi.change.GetChange;
public class Module extends FactoryModule {
@Override
@@ -46,6 +51,11 @@
.to(CheckerRefOperationValidator.class)
.in(SINGLETON);
+ DynamicSet.bind(binder(), ChangeAttributeFactory.class).to(ChangeCheckAttributeFactory.class);
+ bind(DynamicOptions.DynamicBean.class)
+ .annotatedWith(Exports.named(GetChange.class))
+ .to(GetChangeOptions.class);
+
install(new ApiModule());
}
}
diff --git a/java/com/google/gerrit/plugins/checks/PostCheck.java b/java/com/google/gerrit/plugins/checks/PostCheck.java
index 9327842..ed4ce33 100644
--- a/java/com/google/gerrit/plugins/checks/PostCheck.java
+++ b/java/com/google/gerrit/plugins/checks/PostCheck.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
import com.google.gerrit.plugins.checks.api.CheckInfo;
import com.google.gerrit.plugins.checks.api.CheckInput;
import com.google.gerrit.plugins.checks.api.CheckResource;
@@ -78,7 +79,7 @@
CheckerUuid checkerUuid = CheckerUuid.parse(input.checkerUuid);
CheckKey key = CheckKey.create(rsrc.getProject(), rsrc.getPatchSet().getId(), checkerUuid);
- Optional<Check> check = checks.getCheck(key);
+ Optional<Check> check = checks.getCheck(key, GetCheckOptions.defaults());
Check updatedCheck;
if (!check.isPresent()) {
checkers
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
index 3348659..c04a4d3 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/TestModule.java
@@ -14,11 +14,14 @@
package com.google.gerrit.plugins.checks.acceptance;
+import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.plugins.checks.Module;
import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckOperations;
import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckOperationsImpl;
import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations;
import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperationsImpl;
+import com.google.gerrit.plugins.checks.rules.ChecksSubmitRule;
+import com.google.gerrit.server.rules.SubmitRule;
import com.google.inject.AbstractModule;
public class TestModule extends AbstractModule {
@@ -30,5 +33,8 @@
// setup in tests as realistic as possible by delegating to the original module.
bind(CheckerOperations.class).to(CheckerOperationsImpl.class);
bind(CheckOperations.class).to(CheckOperationsImpl.class);
+ bind(SubmitRule.class)
+ .annotatedWith(Exports.named("ChecksSubmitRule"))
+ .to(ChecksSubmitRule.class);
}
}
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
index 9cd2aa5..5d78aae 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImpl.java
@@ -24,6 +24,7 @@
import com.google.gerrit.plugins.checks.CheckUpdate;
import com.google.gerrit.plugins.checks.CheckerRef;
import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
import com.google.gerrit.plugins.checks.ChecksUpdate;
import com.google.gerrit.plugins.checks.ListChecksOption;
import com.google.gerrit.plugins.checks.api.CheckInfo;
@@ -76,12 +77,12 @@
@Override
public boolean exists() throws Exception {
- return checks.getCheck(key).isPresent();
+ return checks.getCheck(key, GetCheckOptions.defaults()).isPresent();
}
@Override
public Check get() throws Exception {
- return checks.getCheck(key).get();
+ return checks.getCheck(key, GetCheckOptions.defaults()).get();
}
@Override
diff --git a/java/com/google/gerrit/plugins/checks/api/BlockingCondition.java b/java/com/google/gerrit/plugins/checks/api/BlockingCondition.java
index 74b43bf..f0d032d 100644
--- a/java/com/google/gerrit/plugins/checks/api/BlockingCondition.java
+++ b/java/com/google/gerrit/plugins/checks/api/BlockingCondition.java
@@ -14,6 +14,8 @@
package com.google.gerrit.plugins.checks.api;
+import java.util.Set;
+
/**
* Conditions evaluated on a check in the context of a change that determine whether the check
* blocks submission of a change.
@@ -24,4 +26,8 @@
* CombinedCheckState#isPassing() is passing}.
*/
STATE_NOT_PASSING;
+
+ public static Boolean isRequired(Set<BlockingCondition> blocking) {
+ return blocking.contains(STATE_NOT_PASSING);
+ }
}
diff --git a/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java b/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java
new file mode 100644
index 0000000..aacbb1e
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/ChangeCheckAttributeFactory.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.server.DynamicOptions.BeanProvider;
+import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangeAttributeFactory;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.kohsuke.args4j.Option;
+
+/**
+ * Factory that populates a single {@link ChangeCheckInfo} instance in the {@code plugins} field of
+ * a {@code ChangeInfo}.
+ */
+@Singleton
+public class ChangeCheckAttributeFactory implements ChangeAttributeFactory {
+ public static class GetChangeOptions implements DynamicBean {
+ @Option(name = "--combined", usage = "include combined check state")
+ boolean combined;
+ }
+
+ private final Checks checks;
+
+ @Inject
+ ChangeCheckAttributeFactory(Checks checks) {
+ this.checks = checks;
+ }
+
+ @Override
+ public ChangeCheckInfo create(ChangeData cd, BeanProvider beanProvider, String plugin) {
+ DynamicBean opts = beanProvider.getDynamicBean(plugin);
+ if (opts == null) {
+ return null;
+ }
+ try {
+ if (opts instanceof GetChangeOptions) {
+ return forGetChange(cd, (GetChangeOptions) opts);
+ }
+ // TODO(dborowitz): Compute from cache in query path.
+ } catch (OrmException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ throw new IllegalStateException("unexpected options type: " + opts);
+ }
+
+ private ChangeCheckInfo forGetChange(ChangeData cd, GetChangeOptions opts)
+ throws OrmException, IOException {
+ if (opts == null || !opts.combined) {
+ return null;
+ }
+ ChangeCheckInfo info = new ChangeCheckInfo();
+ info.combinedState =
+ checks.getCombinedCheckState(cd.project(), cd.change().currentPatchSetId());
+ return info;
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/ChangeCheckInfo.java b/java/com/google/gerrit/plugins/checks/api/ChangeCheckInfo.java
new file mode 100644
index 0000000..257b8fc
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/ChangeCheckInfo.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import java.util.Objects;
+
+/** Information about checks on a change, stored in a plugin-defined field in {@code ChangeInfo}. */
+public class ChangeCheckInfo extends PluginDefinedInfo {
+ /** Combined check state of the change. */
+ public CombinedCheckState combinedState;
+
+ public ChangeCheckInfo() {}
+
+ public ChangeCheckInfo(String pluginName, CombinedCheckState combinedState) {
+ this.name = requireNonNull(pluginName);
+ this.combinedState = requireNonNull(combinedState);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ChangeCheckInfo)) {
+ return false;
+ }
+ ChangeCheckInfo i = (ChangeCheckInfo) o;
+ return Objects.equals(name, i.name) && Objects.equals(combinedState, i.combinedState);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, combinedState);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", name)
+ .add("combinedState", combinedState)
+ .toString();
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java b/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
index 1c34dc4..db02463 100644
--- a/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
+++ b/java/com/google/gerrit/plugins/checks/api/ChecksCollection.java
@@ -26,6 +26,7 @@
import com.google.gerrit.plugins.checks.CheckKey;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
@@ -36,18 +37,13 @@
@Singleton
public class ChecksCollection implements ChildCollection<RevisionResource, CheckResource> {
- private final CheckBackfiller checkBackfiller;
private final Checks checks;
private final DynamicMap<RestView<CheckResource>> views;
private final ListChecks listChecks;
@Inject
ChecksCollection(
- CheckBackfiller checkBackfiller,
- Checks checks,
- DynamicMap<RestView<CheckResource>> views,
- ListChecks listChecks) {
- this.checkBackfiller = checkBackfiller;
+ Checks checks, DynamicMap<RestView<CheckResource>> views, ListChecks listChecks) {
this.checks = checks;
this.views = views;
this.listChecks = listChecks;
@@ -67,12 +63,7 @@
() -> new BadRequestException(String.format("invalid checker UUID: %s", id.get())));
CheckKey checkKey =
CheckKey.create(parent.getProject(), parent.getPatchSet().getId(), checkerUuid);
- Optional<Check> check = checks.getCheck(checkKey);
- if (!check.isPresent()) {
- check =
- checkBackfiller.getBackfilledCheckForRelevantChecker(
- checkerUuid, parent.getNotes(), checkKey.patchSet());
- }
+ Optional<Check> check = checks.getCheck(checkKey, GetCheckOptions.withBackfilling());
return new CheckResource(
parent,
check.orElseThrow(
diff --git a/java/com/google/gerrit/plugins/checks/api/ListChecks.java b/java/com/google/gerrit/plugins/checks/api/ListChecks.java
index a787e69..ed76af0 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListChecks.java
@@ -14,8 +14,6 @@
package com.google.gerrit.plugins.checks.api;
-import static java.util.stream.Collectors.toMap;
-
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -24,23 +22,18 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.plugins.checks.Check;
import com.google.gerrit.plugins.checks.CheckJson;
-import com.google.gerrit.plugins.checks.Checker;
-import com.google.gerrit.plugins.checks.CheckerUuid;
-import com.google.gerrit.plugins.checks.Checkers;
import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
import com.google.gerrit.plugins.checks.ListChecksOption;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.EnumSet;
-import java.util.Map;
import org.kohsuke.args4j.Option;
public class ListChecks implements RestReadView<RevisionResource> {
- private final CheckBackfiller checkBackfiller;
private final CheckJson.Factory checkJsonFactory;
- private final Checkers checkers;
private final Checks checks;
private final EnumSet<ListChecksOption> options = EnumSet.noneOf(ListChecksOption.class);
@@ -56,14 +49,8 @@
}
@Inject
- ListChecks(
- CheckBackfiller checkBackfiller,
- CheckJson.Factory checkJsonFactory,
- Checkers checkers,
- Checks checks) {
- this.checkBackfiller = checkBackfiller;
+ ListChecks(CheckJson.Factory checkJsonFactory, Checks checks) {
this.checkJsonFactory = checkJsonFactory;
- this.checkers = checkers;
this.checks = checks;
}
@@ -71,24 +58,16 @@
public ImmutableList<CheckInfo> apply(RevisionResource resource)
throws AuthException, BadRequestException, ResourceConflictException, OrmException,
IOException {
+ ImmutableList.Builder<CheckInfo> result = ImmutableList.builder();
+
+ GetCheckOptions getCheckOptions = GetCheckOptions.withBackfilling();
+ ImmutableList<Check> allChecks =
+ checks.getChecks(resource.getProject(), resource.getPatchSet().getId(), getCheckOptions);
+
CheckJson checkJson = checkJsonFactory.create(options);
- Map<CheckerUuid, Checker> checkersByUuid =
- checkers.checkersOf(resource.getProject()).stream()
- .collect(toMap(Checker::getUuid, c -> c));
-
- ImmutableList.Builder<CheckInfo> result =
- ImmutableList.builderWithExpectedSize(checkersByUuid.size());
- for (Check check : checks.getChecks(resource.getProject(), resource.getPatchSet().getId())) {
- checkersByUuid.remove(check.key().checkerUuid());
+ for (Check check : allChecks) {
result.add(checkJson.format(check));
}
-
- for (Check check :
- checkBackfiller.getBackfilledChecksForRelevantCheckers(
- checkersByUuid.values(), resource.getNotes(), resource.getPatchSet().getId())) {
- result.add(checkJson.format(check));
- }
-
return result.build();
}
}
diff --git a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
index a4535bc..4ad556c 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
@@ -14,26 +14,44 @@
package com.google.gerrit.plugins.checks.api;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.Check;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerUuid;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.Checks.GetCheckOptions;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Option;
public class ListPendingChecks implements RestReadView<TopLevelResource> {
- private final PermissionBackend permissionBackend;
- private final AdministrateCheckersPermission permission;
+ private final Checkers checkers;
+ private final Checks checks;
+ private final RetryHelper retryHelper;
+ private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+ private final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
private CheckerUuid checkerUuid;
- private String scheme;
private List<CheckState> states = new ArrayList<>(CheckState.values().length);
@Option(
@@ -45,11 +63,6 @@
this.checkerUuid = checkerUuid;
}
- @Option(name = "--scheme", metaVar = "SCHEME", usage = "checker scheme")
- public void setScheme(String scheme) {
- this.scheme = scheme;
- }
-
@Option(name = "--state", metaVar = "STATE", usage = "check state")
public void addState(CheckState state) {
this.states.add(state);
@@ -57,30 +70,92 @@
@Inject
public ListPendingChecks(
- PermissionBackend permissionBackend, AdministrateCheckersPermission permission) {
- this.permissionBackend = permissionBackend;
- this.permission = permission;
+ Checkers checkers,
+ Checks checks,
+ RetryHelper retryHelper,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
+ Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+ this.checkers = checkers;
+ this.checks = checks;
+ this.retryHelper = retryHelper;
+ this.queryBuilderProvider = queryBuilderProvider;
+ this.changeQueryProcessorProvider = changeQueryProcessorProvider;
}
@Override
public List<PendingChecksInfo> apply(TopLevelResource resource)
- throws RestApiException, PermissionBackendException {
- permissionBackend.currentUser().check(permission);
-
+ throws RestApiException, IOException, ConfigInvalidException, OrmException {
if (states.isEmpty()) {
// If no state was specified, assume NOT_STARTED by default.
states.add(CheckState.NOT_STARTED);
}
- if (checkerUuid == null && scheme == null) {
- throw new BadRequestException("checker or scheme is required");
+ if (checkerUuid == null) {
+ throw new BadRequestException("checker UUID is required");
}
- if (checkerUuid != null && scheme != null) {
- throw new BadRequestException("checker and scheme are mutually exclusive");
+ Checker checker =
+ checkers
+ .getChecker(checkerUuid)
+ .orElseThrow(
+ () ->
+ new UnprocessableEntityException(
+ String.format("checker %s not found", checkerUuid)));
+
+ if (checker.getStatus() == CheckerStatus.DISABLED) {
+ return ImmutableList.of();
}
- // TODO(ekempin): Implement this REST endpoint
- throw new MethodNotAllowedException("not implemented");
+ // The query system can only match against the current patch set; ignore non-current patch sets
+ // for now.
+ List<ChangeData> changes =
+ checker.queryMatchingChanges(
+ retryHelper, queryBuilderProvider.get(), changeQueryProcessorProvider);
+ List<PendingChecksInfo> pendingChecks = new ArrayList<>(changes.size());
+ for (ChangeData cd : changes) {
+ getPostFilteredPendingChecks(cd.project(), cd.currentPatchSet().getId())
+ .ifPresent(pendingChecks::add);
+ }
+ return pendingChecks;
+ }
+
+ private Optional<PendingChecksInfo> getPostFilteredPendingChecks(
+ Project.NameKey project, PatchSet.Id patchSetId) throws OrmException, IOException {
+ CheckState checkState = getCheckState(project, patchSetId);
+ if (!states.contains(checkState)) {
+ return Optional.empty();
+ }
+ return Optional.of(createPendingChecksInfo(project, patchSetId, checkerUuid, checkState));
+ }
+
+ private CheckState getCheckState(Project.NameKey project, PatchSet.Id patchSetId)
+ throws OrmException, IOException {
+ Optional<Check> check =
+ checks.getCheck(
+ CheckKey.create(project, patchSetId, checkerUuid), GetCheckOptions.defaults());
+
+ // Backfill if check is not present.
+ // Backfilling is only done for relevant checkers (checkers where the repository and the query
+ // matches the change). Since the change was found by executing the query of the checker we know
+ // that the checker is relevant for this patch set and hence backfilling should be done.
+ return check.map(Check::state).orElse(CheckState.NOT_STARTED);
+ }
+
+ private static PendingChecksInfo createPendingChecksInfo(
+ Project.NameKey project,
+ PatchSet.Id patchSetId,
+ CheckerUuid checkerUuid,
+ CheckState checkState) {
+ PendingChecksInfo pendingChecksInfo = new PendingChecksInfo();
+
+ pendingChecksInfo.patchSet = new CheckablePatchSetInfo();
+ pendingChecksInfo.patchSet.project = project.get();
+ pendingChecksInfo.patchSet.changeNumber = patchSetId.getParentKey().get();
+ pendingChecksInfo.patchSet.patchSetId = patchSetId.get();
+
+ pendingChecksInfo.pendingChecks =
+ ImmutableMap.of(checkerUuid.toString(), new PendingCheckInfo(checkState));
+
+ return pendingChecksInfo;
}
}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java b/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java
index 5e46bde..6ba6275 100644
--- a/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/PendingCheckInfo.java
@@ -14,6 +14,9 @@
package com.google.gerrit.plugins.checks.api;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
/**
* REST API representation of a pending check.
*
@@ -24,4 +27,27 @@
public class PendingCheckInfo {
/** State of the check. */
public CheckState state;
+
+ public PendingCheckInfo(CheckState state) {
+ this.state = state;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(state);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PendingCheckInfo)) {
+ return false;
+ }
+ PendingCheckInfo o = (PendingCheckInfo) obj;
+ return Objects.equals(state, o.state);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("state", state).toString();
+ }
}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecks.java b/java/com/google/gerrit/plugins/checks/api/PendingChecks.java
index 8b0811b..361472b 100644
--- a/java/com/google/gerrit/plugins/checks/api/PendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecks.java
@@ -52,17 +52,6 @@
}
/**
- * Lists the pending checks for all checkers of the specified checker scheme.
- *
- * @param scheme the checker scheme for which pending checks should be listed
- * @param checkStates the states that should be considered as pending, if not specified {@link
- * CheckState#NOT_STARTED} is assumed.
- * @return the pending checks
- */
- List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates)
- throws RestApiException;
-
- /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -71,10 +60,5 @@
public List<PendingChecksInfo> list(CheckerUuid checkerUuid, CheckState... checkStates) {
throw new NotImplementedException();
}
-
- @Override
- public List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates) {
- throw new NotImplementedException();
- }
}
}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java
index 0b9c139..ca4cf18 100644
--- a/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksImpl.java
@@ -46,17 +46,4 @@
throw asRestApiException("Cannot list pending checks", e);
}
}
-
- @Override
- public List<PendingChecksInfo> listForScheme(String scheme, CheckState... checkStates)
- throws RestApiException {
- try {
- ListPendingChecks listPendingChecks = listPendingChecksProvider.get();
- listPendingChecks.setScheme(scheme);
- Stream.of(checkStates).forEach(listPendingChecks::addState);
- return listPendingChecks.apply(TopLevelResource.INSTANCE);
- } catch (Exception e) {
- throw asRestApiException("Cannot list pending checks for scheme", e);
- }
- }
}
diff --git a/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java b/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java
index 01944b6..48311de 100644
--- a/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java
+++ b/java/com/google/gerrit/plugins/checks/api/PendingChecksInfo.java
@@ -14,7 +14,6 @@
package com.google.gerrit.plugins.checks.api;
-import com.google.gerrit.plugins.checks.CheckerUuid;
import java.util.Map;
/** REST API representation of pending checks on patch set. */
@@ -23,5 +22,5 @@
public CheckablePatchSetInfo patchSet;
/** Pending checks on the patch set by checker UUID. */
- public Map<CheckerUuid, PendingCheckInfo> pendingChecks;
+ public Map<String, PendingCheckInfo> pendingChecks;
}
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckBackfiller.java b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
similarity index 61%
rename from java/com/google/gerrit/plugins/checks/api/CheckBackfiller.java
rename to java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
index d25e8af..dfd3892 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckBackfiller.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
@@ -12,26 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.plugins.checks.api;
+package com.google.gerrit.plugins.checks.db;
import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.plugins.checks.Check;
import com.google.gerrit.plugins.checks.CheckKey;
import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeData.Factory;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -43,31 +37,25 @@
@Singleton
class CheckBackfiller {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- private final ChangeData.Factory changeDataFactory;
private final Checkers checkers;
private final Provider<AnonymousUser> anonymousUserProvider;
private final Provider<ChangeQueryBuilder> queryBuilderProvider;
@Inject
CheckBackfiller(
- Factory changeDataFactory,
Checkers checkers,
Provider<AnonymousUser> anonymousUserProvider,
Provider<ChangeQueryBuilder> queryBuilderProvider) {
- this.changeDataFactory = changeDataFactory;
this.checkers = checkers;
this.anonymousUserProvider = anonymousUserProvider;
this.queryBuilderProvider = queryBuilderProvider;
}
ImmutableList<Check> getBackfilledChecksForRelevantCheckers(
- Collection<Checker> candidates, ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+ Collection<Checker> candidates, ChangeData cd, PatchSet.Id psId) throws OrmException {
if (candidates.isEmpty()) {
return ImmutableList.of();
}
- ChangeData cd = changeDataFactory.create(notes);
if (!psId.equals(cd.change().currentPatchSetId())) {
// The query system can only match against the current patch set; it doesn't make sense to
// backfill checkers for old patch sets.
@@ -79,7 +67,7 @@
PatchSet ps = cd.patchSet(psId);
ChangeQueryBuilder queryBuilder = newQueryBuilder();
for (Checker checker : candidates) {
- if (matches(checker, cd, queryBuilder)) {
+ if (checker.isCheckerRelevant(cd, queryBuilder)) {
// Add synthetic check at the creation time of the patch set.
result.add(newBackfilledCheck(cd, ps, checker));
}
@@ -88,8 +76,7 @@
}
Optional<Check> getBackfilledCheckForRelevantChecker(
- CheckerUuid candidate, ChangeNotes notes, PatchSet.Id psId) throws OrmException, IOException {
- ChangeData cd = changeDataFactory.create(notes);
+ CheckerUuid candidate, ChangeData cd, PatchSet.Id psId) throws OrmException, IOException {
if (!psId.equals(cd.change().currentPatchSetId())) {
// The query system can only match against the current patch set; it doesn't make sense to
// backfill checkers for old patch sets.
@@ -105,7 +92,7 @@
}
if (!checker.isPresent()
|| checker.get().getStatus() != CheckerStatus.ENABLED
- || !matches(checker.get(), cd, newQueryBuilder())) {
+ || !checker.get().isCheckerRelevant(cd, newQueryBuilder())) {
return Optional.empty();
}
return Optional.of(newBackfilledCheck(cd, cd.patchSet(psId), checker.get()));
@@ -122,41 +109,4 @@
private ChangeQueryBuilder newQueryBuilder() {
return queryBuilderProvider.get().asUser(anonymousUserProvider.get());
}
-
- private static boolean matches(Checker checker, ChangeData cd, ChangeQueryBuilder queryBuilder)
- throws OrmException {
- if (!checker.getQuery().isPresent()) {
- return cd.change().isNew();
- }
- String query = checker.getQuery().get();
- Predicate<ChangeData> predicate;
- try {
- predicate = queryBuilder.parse(query);
- } catch (QueryParseException e) {
- logger.atWarning().withCause(e).log(
- "skipping invalid query for checker %s: %s", checker.getUuid(), query);
- return false;
- }
- if (!predicate.isMatchable()) {
- // Assuming nobody modified the query behind Gerrit's back, this is programmer error:
- // CheckerQuery should not be able to produce non-matchable queries.
- logger.atWarning().log(
- "skipping non-matchable query for checker %s: %s", checker.getUuid(), query);
- return false;
- }
- if (!hasStatusPredicate(predicate)) {
- predicate = Predicate.and(ChangeStatusPredicate.open(), predicate);
- }
- return predicate.asMatchable().match(cd);
- }
-
- private static boolean hasStatusPredicate(Predicate<ChangeData> predicate) {
- if (predicate instanceof IndexPredicate) {
- return ((IndexPredicate<ChangeData>) predicate)
- .getField()
- .getName()
- .equals(ChangeField.STATUS.getName());
- }
- return predicate.getChildren().stream().anyMatch(CheckBackfiller::hasStatusPredicate);
- }
}
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
index 66432ba..09fdcb2 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckersByRepositoryNotes.java
@@ -330,9 +330,7 @@
* <p>Doesn't validate the entries are valid checker UUIDs.
*/
private static ImmutableSortedSet<String> parseNote(byte[] raw) {
- return Splitter.on('\n')
- .splitToList(new String(raw, UTF_8))
- .stream()
+ return Splitter.on('\n').splitToList(new String(raw, UTF_8)).stream()
.collect(toImmutableSortedSet(naturalOrder()));
}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
index f6c0c52..be8bd8e 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckers.java
@@ -63,10 +63,7 @@
@Override
public ImmutableList<Checker> listCheckers() throws IOException {
try (Repository allProjectsRepo = repoManager.openRepository(allProjectsName)) {
- return allProjectsRepo
- .getRefDatabase()
- .getRefsByPrefix(CheckerRef.REFS_CHECKERS)
- .stream()
+ return allProjectsRepo.getRefDatabase().getRefsByPrefix(CheckerRef.REFS_CHECKERS).stream()
.flatMap(ref -> Streams.stream(tryLoadChecker(allProjectsRepo, ref)))
.sorted(comparing(Checker::getUuid))
.collect(toImmutableList());
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
index c856c54..6da5a2a 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersUpdate.java
@@ -162,8 +162,13 @@
CheckersByRepositoryNotes checkersByRepositoryNotes =
CheckersByRepositoryNotes.load(allProjectsName, allProjectsRepo);
- checkersByRepositoryNotes.insert(
- checkerCreation.getCheckerUuid(), checkerCreation.getRepository());
+ if (!checkerUpdate.getStatus().isPresent()
+ || checkerUpdate.getStatus().get() == CheckerStatus.ENABLED) {
+ // Only inserts to the notes if the status is not set or set as "ENABLED". Does not insert
+ // if the checker is DISABLED.
+ checkersByRepositoryNotes.insert(
+ checkerCreation.getCheckerUuid(), checkerCreation.getRepository());
+ }
commit(allProjectsRepo, checkerConfig, checkersByRepositoryNotes);
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
index 81779ce..c01b359 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
@@ -15,67 +15,166 @@
package com.google.gerrit.plugins.checks.db;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.plugins.checks.Check;
import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checkers;
import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
/** Class to read checks from NoteDb. */
@Singleton
class NoteDbChecks implements Checks {
- private final ChangeNotes.Factory changeNotesFactory;
- private final PatchSetUtil psUtil;
+ private final ChangeData.Factory changeDataFactory;
private final CheckNotes.Factory checkNotesFactory;
+ private final Checkers checkers;
+ private final CheckBackfiller checkBackfiller;
+ private final Provider<AnonymousUser> anonymousUserProvider;
+ private final Provider<ChangeQueryBuilder> queryBuilderProvider;
@Inject
NoteDbChecks(
- ChangeNotes.Factory changeNotesFactory,
- PatchSetUtil psUtil,
- CheckNotes.Factory checkNotesFactory) {
- this.changeNotesFactory = changeNotesFactory;
- this.psUtil = psUtil;
+ ChangeData.Factory changeDataFactory,
+ CheckNotes.Factory checkNotesFactory,
+ Checkers checkers,
+ CheckBackfiller checkBackfiller,
+ Provider<AnonymousUser> anonymousUserProvider,
+ Provider<ChangeQueryBuilder> queryBuilderProvider) {
+ this.changeDataFactory = changeDataFactory;
this.checkNotesFactory = checkNotesFactory;
+ this.checkers = checkers;
+ this.checkBackfiller = checkBackfiller;
+ this.anonymousUserProvider = anonymousUserProvider;
+ this.queryBuilderProvider = queryBuilderProvider;
}
@Override
- public ImmutableList<Check> getChecks(Project.NameKey projectName, PatchSet.Id psId)
- throws OrmException {
- return getChecksAsStream(projectName, psId).collect(toImmutableList());
+ public ImmutableList<Check> getChecks(
+ Project.NameKey projectName, PatchSet.Id psId, GetCheckOptions options)
+ throws IOException, OrmException {
+ return getChecksFromNoteDb(projectName, psId, options);
}
@Override
- public Optional<Check> getCheck(CheckKey checkKey) throws OrmException {
+ public Optional<Check> getCheck(CheckKey checkKey, GetCheckOptions options)
+ throws OrmException, IOException {
// TODO(gerrit-team): Instead of reading the complete notes map, read just one note.
- return getChecksAsStream(checkKey.project(), checkKey.patchSet())
- .filter(c -> c.key().checkerUuid().equals(checkKey.checkerUuid()))
- .findAny();
+ Optional<Check> result =
+ getChecksFromNoteDb(checkKey.project(), checkKey.patchSet(), GetCheckOptions.defaults())
+ .stream()
+ .filter(c -> c.key().checkerUuid().equals(checkKey.checkerUuid()))
+ .findAny();
+
+ if (!result.isPresent() && options.backfillChecks()) {
+ ChangeData changeData =
+ changeDataFactory.create(checkKey.project(), checkKey.patchSet().getParentKey());
+ return checkBackfiller.getBackfilledCheckForRelevantChecker(
+ checkKey.checkerUuid(), changeData, checkKey.patchSet());
+ }
+
+ return result;
}
- private Stream<Check> getChecksAsStream(Project.NameKey projectName, PatchSet.Id psId)
- throws OrmException {
+ private ImmutableList<Check> getChecksFromNoteDb(
+ Project.NameKey projectName, PatchSet.Id psId, GetCheckOptions options)
+ throws OrmException, IOException {
// TODO(gerrit-team): Instead of reading the complete notes map, read just one note.
- ChangeNotes notes = changeNotesFactory.create(projectName, psId.getParentKey());
- PatchSet patchSet = psUtil.get(notes, psId);
- CheckNotes checkNotes = checkNotesFactory.create(notes.getChange());
-
+ ChangeData changeData = changeDataFactory.create(projectName, psId.getParentKey());
+ PatchSet patchSet = changeData.patchSet(psId);
+ CheckNotes checkNotes = checkNotesFactory.create(changeData.change());
checkNotes.load();
- return checkNotes
- .getChecks()
- .getOrDefault(patchSet.getRevision(), NoteDbCheckMap.empty())
- .checks
- .entrySet()
- .stream()
- .map(e -> e.getValue().toCheck(projectName, psId, CheckerUuid.parse(e.getKey())));
+
+ ImmutableList<Check> existingChecks =
+ checkNotes.getChecks().getOrDefault(patchSet.getRevision(), NoteDbCheckMap.empty()).checks
+ .entrySet().stream()
+ .map(e -> e.getValue().toCheck(projectName, psId, CheckerUuid.parse(e.getKey())))
+ .collect(toImmutableList());
+
+ if (!options.backfillChecks()) {
+ return existingChecks;
+ }
+
+ ImmutableList<Checker> checkersForBackfiller =
+ getCheckersForBackfiller(projectName, existingChecks);
+ ImmutableList<Check> backfilledChecks =
+ checkBackfiller.getBackfilledChecksForRelevantCheckers(
+ checkersForBackfiller, changeData, psId);
+
+ return Stream.concat(existingChecks.stream(), backfilledChecks.stream())
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public CombinedCheckState getCombinedCheckState(NameKey projectName, Id patchSetId)
+ throws IOException, OrmException {
+ ChangeData changeData = changeDataFactory.create(projectName, patchSetId.changeId);
+ ImmutableMap<String, Checker> allCheckersOfProject =
+ checkers.checkersOf(projectName).stream()
+ .collect(ImmutableMap.toImmutableMap(c -> c.getUuid().toString(), c -> c));
+
+ // Always backfilling checks to have a meaningful "CombinedCheckState" even when there are some
+ // or all checks missing.
+ ImmutableMap<String, Check> checks =
+ getChecks(projectName, patchSetId, GetCheckOptions.withBackfilling()).stream()
+ .collect(ImmutableMap.toImmutableMap(c -> c.key().checkerUuid().toString(), c -> c));
+
+ ChangeQueryBuilder queryBuilder =
+ queryBuilderProvider.get().asUser(anonymousUserProvider.get());
+ ImmutableListMultimap.Builder<CheckState, Boolean> statesAndRequired =
+ ImmutableListMultimap.builder();
+
+ for (Map.Entry<String, Check> entry : checks.entrySet()) {
+ String checkerUuid = entry.getKey();
+ Check check = entry.getValue();
+
+ Checker checker = allCheckersOfProject.get(checkerUuid);
+ if (checker == null) {
+ // A not-relevant check.
+ statesAndRequired.put(check.state(), false);
+ continue;
+ }
+
+ boolean isRequired =
+ checker.getStatus() == CheckerStatus.ENABLED
+ && checker.isRequired()
+ && checker.isCheckerRelevant(changeData, queryBuilder);
+ statesAndRequired.put(check.state(), isRequired);
+ }
+
+ return CombinedCheckState.combine(statesAndRequired.build());
+ }
+
+ private ImmutableList<Checker> getCheckersForBackfiller(
+ Project.NameKey projectName, List<Check> existingChecks) throws IOException {
+ ImmutableSet<CheckerUuid> checkersWithExistingChecks =
+ existingChecks.stream().map(c -> c.key().checkerUuid()).collect(toImmutableSet());
+ return checkers.checkersOf(projectName).stream()
+ .filter(c -> !checkersWithExistingChecks.contains(c.getUuid()))
+ .collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java b/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java
new file mode 100644
index 0000000..30b5b5c
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/rules/ChecksSubmitRule.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2019 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.plugins.checks.rules;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRecord.Status;
+import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+
+@Singleton
+public class ChecksSubmitRule implements SubmitRule {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final SubmitRequirement DEFAULT_SUBMIT_REQUIREMENT_FOR_CHECKS =
+ SubmitRequirement.builder()
+ .setFallbackText("All required checks must pass")
+ .setType("checks_pass")
+ .build();
+
+ public static class Module extends FactoryModule {
+ @Override
+ public void configure() {
+ bind(SubmitRule.class)
+ .annotatedWith(Exports.named("ChecksSubmitRule"))
+ .to(ChecksSubmitRule.class);
+ }
+ }
+
+ private final Checks checks;
+
+ @Inject
+ public ChecksSubmitRule(Checks checks) {
+ this.checks = checks;
+ }
+
+ @Override
+ public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
+ Project.NameKey project = changeData.project();
+ Change.Id changeId = changeData.getId();
+
+ PatchSet.Id currentPathSetId;
+ try {
+ currentPathSetId = changeData.currentPatchSet().getId();
+ } catch (OrmException e) {
+ String errorMessage =
+ String.format("failed to load the current patch set of change %s", changeId);
+ logger.atSevere().withCause(e).log(errorMessage);
+ return singletonRecordForRuleError(errorMessage);
+ }
+
+ CombinedCheckState combinedCheckState;
+ try {
+ combinedCheckState = checks.getCombinedCheckState(project, currentPathSetId);
+ } catch (IOException | OrmException e) {
+ String errorMessage =
+ String.format("failed to evaluate check states for change %s", changeId);
+ logger.atSevere().withCause(e).log(errorMessage);
+ return singletonRecordForRuleError(errorMessage);
+ }
+
+ SubmitRecord submitRecord = new SubmitRecord();
+ if (combinedCheckState.isPassing()) {
+ submitRecord.status = Status.OK;
+ return ImmutableList.of(submitRecord);
+ }
+
+ submitRecord.status = Status.NOT_READY;
+ submitRecord.requirements = ImmutableList.of(DEFAULT_SUBMIT_REQUIREMENT_FOR_CHECKS);
+ return ImmutableSet.of(submitRecord);
+ }
+
+ private static Collection<SubmitRecord> singletonRecordForRuleError(String reason) {
+ SubmitRecord submitRecord = new SubmitRecord();
+ submitRecord.errorMessage = reason;
+ submitRecord.status = SubmitRecord.Status.RULE_ERROR;
+ return ImmutableList.of(submitRecord);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java b/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java
new file mode 100644
index 0000000..72c6269
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/testing/PendingChecksInfoSubject.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2019 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.plugins.checks.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.MapSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.checks.api.CheckablePatchSetInfo;
+import com.google.gerrit.plugins.checks.api.PendingCheckInfo;
+import com.google.gerrit.plugins.checks.api.PendingChecksInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import java.util.Map;
+
+public class PendingChecksInfoSubject extends Subject<PendingChecksInfoSubject, PendingChecksInfo> {
+ public static PendingChecksInfoSubject assertThat(PendingChecksInfo pendingChecksInfo) {
+ return assertAbout(PendingChecksInfoSubject::new).that(pendingChecksInfo);
+ }
+
+ private PendingChecksInfoSubject(FailureMetadata metadata, PendingChecksInfo actual) {
+ super(metadata, actual);
+ }
+
+ public void hasProject(Project.NameKey expectedProject) {
+ check("patchSet().project()").that(patchSet().project).isEqualTo(expectedProject.get());
+ }
+
+ public void hasPatchSet(PatchSet.Id expectedPatchSetId) {
+ CheckablePatchSetInfo patchSet = patchSet();
+ check("patchSet().changeNumber()")
+ .that(patchSet.changeNumber)
+ .isEqualTo(expectedPatchSetId.getParentKey().get());
+ check("patchSet().id()").that(patchSet.patchSetId).isEqualTo(expectedPatchSetId.get());
+ }
+
+ public MapSubject hasPendingChecksMapThat() {
+ return check("pendingChecks()").that(pendingChecks());
+ }
+
+ private CheckablePatchSetInfo patchSet() {
+ isNotNull();
+ CheckablePatchSetInfo patchSet = actual().patchSet;
+ check("patchSet()").that(patchSet).isNotNull();
+ return patchSet;
+ }
+
+ private Map<String, PendingCheckInfo> pendingChecks() {
+ isNotNull();
+ Map<String, PendingCheckInfo> pendingChecks = actual().pendingChecks;
+ check("pendingChecks()").that(pendingChecks).isNotNull();
+ return pendingChecks;
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/BUILD b/javatests/com/google/gerrit/plugins/checks/BUILD
index d87309f..814e5b8 100644
--- a/javatests/com/google/gerrit/plugins/checks/BUILD
+++ b/javatests/com/google/gerrit/plugins/checks/BUILD
@@ -11,6 +11,7 @@
"//java/com/google/gerrit/index:query_exception",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/util/time",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib/guice",
diff --git a/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java b/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java
new file mode 100644
index 0000000..7b9481e
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/CheckerDefinitionTest.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2019 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.plugins.checks;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.checks.api.BlockingCondition.STATE_NOT_PASSING;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.plugins.checks.api.BlockingCondition;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.util.EnumSet;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class CheckerDefinitionTest {
+
+ @Test
+ public void notRequiredIfNoBlockingCondition() {
+ Checker checker = newChecker().setBlockingConditions(ImmutableSortedSet.of()).build();
+
+ assertThat(checker.isRequired()).isFalse();
+ }
+
+ @Test
+ public void requiredIfHasBlockingConditionStateNotPassing() {
+ Checker checker =
+ newChecker().setBlockingConditions(ImmutableSortedSet.of(STATE_NOT_PASSING)).build();
+
+ assertThat(checker.isRequired()).isTrue();
+ }
+
+ @Test
+ public void allBlockingConditionsConsidered() {
+ assertThat(EnumSet.allOf(BlockingCondition.class)).containsExactly(STATE_NOT_PASSING);
+ }
+
+ private Checker.Builder newChecker() {
+ return Checker.builder()
+ .setRepository(new NameKey("test-repo"))
+ .setStatus(CheckerStatus.ENABLED)
+ .setUuid(CheckerUuid.parse("schema:any-id"))
+ .setCreatedOn(TimeUtil.nowTs())
+ .setUpdatedOn(TimeUtil.nowTs())
+ .setRefState(ObjectId.zeroId());
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
index 478eed1..af4ad01 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckerRefsIT.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.plugins.checks.CheckerRef;
import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.notedb.Sequences;
@@ -101,9 +102,9 @@
@Test
public void submitToCheckerRefsIsDisabled() throws Exception {
- CheckerUuid checkerUuid = checkerOperations.newChecker().create();
+ CheckerUuid checkerUuid =
+ checkerOperations.newChecker().status(CheckerStatus.DISABLED).create();
String checkerRef = checkerUuid.toRefName();
-
String changeId = createChangeWithoutCommitValidation(checkerRef);
grantLabel(
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckersIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckersIT.java
index 9e47902..d351813 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/CheckersIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/CheckersIT.java
@@ -19,6 +19,7 @@
import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.Checkers;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.Project;
import java.util.stream.Stream;
import org.junit.Test;
@@ -45,19 +46,19 @@
@Test
public void checkersOfOmitsDisabledCheckers() throws Exception {
+ // Creates a disabled checker.
+ checkerOperations.newChecker().repository(project).status(CheckerStatus.DISABLED).create();
+ // Creates an enabled checker and then disabled it by an update.
CheckerUuid checkerUuid1 = checkerOperations.newChecker().repository(project).create();
- CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
checkerOperations.checker(checkerUuid1).forUpdate().disable().update();
+ // Creates an enabled checker.
+ CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
assertThat(getCheckerUuidsOf(project)).containsExactly(checkerUuid2);
}
private Stream<CheckerUuid> getCheckerUuidsOf(Project.NameKey projectName) throws Exception {
- return plugin
- .getSysInjector()
- .getInstance(Checkers.class)
- .checkersOf(projectName)
- .stream()
+ return plugin.getSysInjector().getInstance(Checkers.class).checkersOf(projectName).stream()
.map(Checker::getUuid);
}
}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD b/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD
index 7db608a..b341d12 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/BUILD
@@ -13,5 +13,6 @@
"//plugins/checks:checks__plugin",
"//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance",
"//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/testing",
],
)
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java
new file mode 100644
index 0000000..771a581
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2019 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.plugins.checks.acceptance.api;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.plugins.checks.api.BlockingCondition.STATE_NOT_PASSING;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.api.ChangeCheckInfo;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChangeCheckInfoIT extends AbstractCheckersTest {
+ private Change.Id changeId;
+ private PatchSet.Id psId;
+
+ @Before
+ public void setUp() throws Exception {
+ psId = createChange().getPatchSetId();
+ changeId = psId.getParentKey();
+ }
+
+ @Test
+ public void infoRequiresOption() throws Exception {
+ assertThat(
+ getChangeCheckInfo(gApi.changes().id(changeId.get()).get(ImmutableListMultimap.of())))
+ .isEmpty();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.NOT_RELEVANT));
+ }
+
+ @Test
+ public void noCheckers() throws Exception {
+ assertThat(checkerOperations.checkersOf(project)).isEmpty();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.NOT_RELEVANT));
+ }
+
+ @Test
+ public void backfillRelevantChecker() throws Exception {
+ checkerOperations.newChecker().repository(project).query("topic:foo").create();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.NOT_RELEVANT));
+ gApi.changes().id(changeId.get()).topic("foo");
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.IN_PROGRESS));
+ }
+
+ @Test
+ public void combinedCheckState() throws Exception {
+ CheckerUuid optionalCheckerUuid = checkerOperations.newChecker().repository(project).create();
+ CheckerUuid requiredCheckerUuid =
+ checkerOperations
+ .newChecker()
+ .repository(project)
+ .blockingConditions(STATE_NOT_PASSING)
+ .create();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.IN_PROGRESS));
+ checkOperations
+ .newCheck(CheckKey.create(project, psId, optionalCheckerUuid))
+ .setState(CheckState.SUCCESSFUL)
+ .upsert();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.IN_PROGRESS));
+ checkOperations
+ .newCheck(CheckKey.create(project, psId, requiredCheckerUuid))
+ .setState(CheckState.FAILED)
+ .upsert();
+ assertThat(getChangeCheckInfo(changeId))
+ .hasValue(new ChangeCheckInfo("checks", CombinedCheckState.FAILED));
+ }
+
+ private Optional<ChangeCheckInfo> getChangeCheckInfo(Change.Id id) throws Exception {
+ return getChangeCheckInfo(
+ gApi.changes().id(id.get()).get(ImmutableListMultimap.of("checks--combined", "true")));
+ }
+
+ private static Optional<ChangeCheckInfo> getChangeCheckInfo(ChangeInfo changeInfo) {
+ if (changeInfo.plugins == null) {
+ return Optional.empty();
+ }
+ ImmutableList<PluginDefinedInfo> infos =
+ changeInfo.plugins.stream().filter(i -> i.name.equals("checks")).collect(toImmutableList());
+ if (infos.isEmpty()) {
+ return Optional.empty();
+ }
+ assertThat(infos).hasSize(1);
+ assertThat(infos.get(0)).isInstanceOf(ChangeCheckInfo.class);
+ return Optional.of((ChangeCheckInfo) infos.get(0));
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java
index 9f4b267..52c7334 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListCheckersIT.java
@@ -48,8 +48,7 @@
.url("http://example.com/my-checker")
.create();
List<CheckerInfo> expectedCheckerInfos =
- ImmutableList.of(checkerUuid1, checkerUuid2, checkerUuid3)
- .stream()
+ ImmutableList.of(checkerUuid1, checkerUuid2, checkerUuid3).stream()
.sorted()
.map(uuid -> checkerOperations.checker(uuid).asInfo())
.collect(toList());
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
index eded791..ec925bc 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListChecksIT.java
@@ -85,15 +85,14 @@
Collection<CheckInfo> info =
checksApiFactory.revision(patchSetId).list(ListChecksOption.CHECKER);
- Timestamp psCreated = getPatchSetCreated(patchSetId.getParentKey());
CheckInfo expected1 = new CheckInfo();
expected1.project = checkKey1.project().get();
expected1.changeNumber = checkKey1.patchSet().getParentKey().get();
expected1.patchSetId = checkKey1.patchSet().get();
expected1.checkerUuid = checkKey1.checkerUuid().toString();
expected1.state = CheckState.RUNNING;
- expected1.created = psCreated;
- expected1.updated = psCreated;
+ expected1.created = checkOperations.check(checkKey1).get().created();
+ expected1.updated = expected1.created;
expected1.blocking = ImmutableSet.of();
expected1.checkerStatus = CheckerStatus.ENABLED;
@@ -103,8 +102,8 @@
expected2.patchSetId = checkKey2.patchSet().get();
expected2.checkerUuid = checkKey2.checkerUuid().toString();
expected2.state = CheckState.RUNNING;
- expected2.created = psCreated;
- expected2.updated = psCreated;
+ expected2.created = checkOperations.check(checkKey2).get().created();
+ expected2.updated = expected2.created;
expected2.checkerName = "Checker Two";
expected2.blocking = ImmutableSet.of();
expected2.checkerStatus = CheckerStatus.ENABLED;
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
index 7cb8d53..5c7d2d4 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
@@ -15,24 +15,61 @@
package com.google.gerrit.plugins.checks.acceptance.api;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.checks.testing.PendingChecksInfoSubject.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.PendingCheckInfo;
+import com.google.gerrit.plugins.checks.api.PendingChecksInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.testing.TestTimeUtil;
import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
public class ListPendingChecksIT extends AbstractCheckersTest {
@Inject private RequestScopeOperations requestScopeOperations;
+ private PatchSet.Id patchSetId;
+
+ @Before
+ public void setUp() throws Exception {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+
+ patchSetId = createChange().getPatchSetId();
+ }
+
+ @After
+ public void resetTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
@Test
- public void specifyingEitherCheckerUuidOrSchemeIsRequired() throws Exception {
- exception.expect(BadRequestException.class);
- exception.expectMessage("checker or scheme is required");
- pendingChecksApi.listForScheme(null);
+ public void specifyingCheckerUuidIsRequired() throws Exception {
+ // The extension API doesn't allow to not specify a checker UUID. Call the endpoint over REST to
+ // test this.
+ RestResponse response = adminRestSession.get("/plugins/checks/checks.pending/");
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("checker UUID is required");
}
@Test
@@ -43,37 +80,306 @@
}
@Test
- public void cannotSpecifyCheckerUuidAndScheme() throws Exception {
- // The extension API doesn't allow to specify checker UUID and scheme at the same time. Call the
- // endpoint over REST to test this.
- RestResponse response =
- adminRestSession.get(
- String.format(
- "/plugins/checks/checks.pending/?checker=%s&scheme=%s", "foo:bar", "foo"));
- response.assertBadRequest();
- assertThat(response.getEntityContent()).isEqualTo("checker and scheme are mutually exclusive");
+ public void cannotListPendingChecksForNonExistingChecker() throws Exception {
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("checker non:existing not found");
+ pendingChecksApi.list("non:existing");
}
@Test
- public void cannotListPendingChecksWithoutAdministrateCheckers() throws Exception {
+ public void listPendingChecksNotStartedStateAssumed() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+ // Create a check with state "NOT_STARTED" that we expect to be returned.
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Create a check with state "FAILED" that we expect to be ignored.
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .setState(CheckState.FAILED)
+ .upsert();
+
+ // Create a check with state "NOT_STARTED" for other checker that we expect to be ignored.
+ CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid2))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList = pendingChecksApi.list(checkerUuid);
+ assertThat(pendingChecksList).hasSize(1);
+ PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
+ assertThat(pendingChecks).hasProject(project);
+ assertThat(pendingChecks).hasPatchSet(patchSetId);
+ assertThat(pendingChecks)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.NOT_STARTED));
+ }
+
+ @Test
+ public void listPendingChecksForSpecifiedState() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+ // Create a check with state "FAILED" that we expect to be returned.
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.FAILED)
+ .upsert();
+
+ // Create a check with state "NOT_STARTED" that we expect to be ignored.
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Create a check with state "FAILED" for other checker that we expect to be ignored.
+ CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid2))
+ .setState(CheckState.FAILED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.FAILED);
+ assertThat(pendingChecksList).hasSize(1);
+ PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
+ assertThat(pendingChecks).hasProject(project);
+ assertThat(pendingChecks).hasPatchSet(patchSetId);
+ assertThat(pendingChecks)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.FAILED));
+ }
+
+ @Test
+ public void listPendingChecksForMultipleSpecifiedStates() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+
+ // Create a check with state "NOT_STARTED" that we expect to be returned.
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Create a check with state "SCHEDULED" that we expect to be returned.
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .setState(CheckState.SCHEDULED)
+ .upsert();
+
+ // Create a check with state "SUCCESSFUL" that we expect to be ignored.
+ PatchSet.Id patchSetId3 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId3, checkerUuid))
+ .setState(CheckState.SUCCESSFUL)
+ .upsert();
+
+ // Create a check with state "NOT_STARTED" for other checker that we expect to be ignored.
+ CheckerUuid checkerUuid2 = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid2))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED, CheckState.SCHEDULED);
+ assertThat(pendingChecksList).hasSize(2);
+
+ // The sorting of the pendingChecksList matches the sorting in which the matching changes are
+ // returned from the change index, which is by last updated timestamp. Use this knowledge here
+ // to do the assertions although the REST endpoint doesn't document a guaranteed sort order.
+ PendingChecksInfo pendingChecksChange2 = pendingChecksList.get(0);
+ assertThat(pendingChecksChange2).hasProject(project);
+ assertThat(pendingChecksChange2).hasPatchSet(patchSetId2);
+ assertThat(pendingChecksChange2)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.SCHEDULED));
+
+ PendingChecksInfo pendingChecksChange1 = pendingChecksList.get(1);
+ assertThat(pendingChecksChange1).hasProject(project);
+ assertThat(pendingChecksChange1).hasPatchSet(patchSetId);
+ assertThat(pendingChecksChange1)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.NOT_STARTED));
+ }
+
+ @Test
+ public void backfillForApplyingChecker() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ List<PendingChecksInfo> pendingChecksList = pendingChecksApi.list(checkerUuid);
+ assertThat(pendingChecksList).hasSize(1);
+ PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
+ assertThat(pendingChecks).hasProject(project);
+ assertThat(pendingChecks).hasPatchSet(patchSetId);
+ assertThat(pendingChecks)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.NOT_STARTED));
+ }
+
+ @Test
+ public void noBackfillForCheckerThatDoesNotApplyToTheProject() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(allProjects).create();
+ assertThat(pendingChecksApi.list(checkerUuid)).isEmpty();
+ }
+
+ @Test
+ public void noBackfillForCheckerThatDoesNotApplyToTheChange() throws Exception {
+ CheckerUuid checkerUuid =
+ checkerOperations.newChecker().repository(project).query("message:not-matching").create();
+ assertThat(pendingChecksApi.list(checkerUuid)).isEmpty();
+ }
+
+ @Test
+ public void listPendingChecksForDisabledChecker() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isNotEmpty();
+
+ checkerOperations.checker(checkerUuid).forUpdate().disable().update();
+ pendingChecksList = pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isEmpty();
+ }
+
+ @Test
+ public void listPendingChecksFiltersOutChecksForClosedChangesIfQueryDoesntSpecifyStatus()
+ throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).query("").create();
+
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).hasSize(2);
+
+ gApi.changes().id(patchSetId2.getParentKey().toString()).abandon();
+
+ pendingChecksList = pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).hasSize(1);
+ }
+
+ @Test
+ public void listPendingChecksReturnsChecksForClosedChangesIfQuerySpecifiesStatus()
+ throws Exception {
+ CheckerUuid checkerUuid =
+ checkerOperations.newChecker().repository(project).query("is:open OR is:closed").create();
+
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ PatchSet.Id patchSetId2 = createChange().getPatchSetId();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId2, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).hasSize(2);
+
+ gApi.changes().id(patchSetId2.getParentKey().toString()).abandon();
+
+ pendingChecksList = pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).hasSize(2);
+ }
+
+ @Test
+ public void listPendingChecksForInvalidCheckerFails() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ checkerOperations.checker(checkerUuid).forUpdate().forceInvalidConfig().update();
+
+ exception.expect(RestApiException.class);
+ exception.expectMessage("Cannot list pending checks");
+ exception.expectCause(instanceOf(ConfigInvalidException.class));
+ pendingChecksApi.list(checkerUuid);
+ }
+
+ @Test
+ public void listPendingChecksWithoutAdministrateCheckersCapabilityWorks() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
requestScopeOperations.setApiUser(user.getId());
-
- exception.expect(AuthException.class);
- exception.expectMessage("not permitted");
- pendingChecksApi.list("foo:bar");
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).hasSize(1);
+ PendingChecksInfo pendingChecks = Iterables.getOnlyElement(pendingChecksList);
+ assertThat(pendingChecks).hasProject(project);
+ assertThat(pendingChecks).hasPatchSet(patchSetId);
+ assertThat(pendingChecks)
+ .hasPendingChecksMapThat()
+ .containsExactly(checkerUuid.toString(), new PendingCheckInfo(CheckState.NOT_STARTED));
}
@Test
- public void listPendingChecksForCheckerNotImplemented() throws Exception {
- exception.expect(MethodNotAllowedException.class);
- exception.expectMessage("not implemented");
- pendingChecksApi.list("foo:bar");
+ public void pendingChecksDontIncludeChecksForNonVisibleChanges() throws Exception {
+ // restrict project visibility so that it is only visible to administrators
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
+
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Check is returned for admin user.
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isNotEmpty();
+
+ // Check is not returned for non-admin user.
+ requestScopeOperations.setApiUser(user.getId());
+ pendingChecksList = pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isEmpty();
}
@Test
- public void listPendingChecksForSchemeNotImplemented() throws Exception {
- exception.expect(MethodNotAllowedException.class);
- exception.expectMessage("not implemented");
- pendingChecksApi.listForScheme("foo");
+ public void pendingChecksDontIncludeChecksForPrivateChangesOfOtherUsers() throws Exception {
+ // make change private so that it is only visible to the admin user
+ gApi.changes().id(patchSetId.getParentKey().get()).setPrivate(true);
+
+ CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+ checkOperations
+ .newCheck(CheckKey.create(project, patchSetId, checkerUuid))
+ .setState(CheckState.NOT_STARTED)
+ .upsert();
+
+ // Check is returned for admin user.
+ List<PendingChecksInfo> pendingChecksList =
+ pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isNotEmpty();
+
+ // Check is not returned for non-admin user.
+ requestScopeOperations.setApiUser(user.getId());
+ pendingChecksList = pendingChecksApi.list(checkerUuid, CheckState.NOT_STARTED);
+ assertThat(pendingChecksList).isEmpty();
}
}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/db/BUILD b/javatests/com/google/gerrit/plugins/checks/acceptance/db/BUILD
new file mode 100644
index 0000000..03caa7d
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/db/BUILD
@@ -0,0 +1,17 @@
+package(default_visibility = ["//plugins/checks:visibility"])
+
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "get_combined_check_state",
+ labels = [
+ "get_combined_check_state",
+ ],
+ deps = [
+ "//java/com/google/gerrit/git/testing",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/db/GetCombinedCheckStateIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/db/GetCombinedCheckStateIT.java
new file mode 100644
index 0000000..0eb82ae
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/db/GetCombinedCheckStateIT.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2019 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.plugins.checks.acceptance.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.Checks;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.TestCheckerCreation;
+import com.google.gerrit.plugins.checks.api.BlockingCondition;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GetCombinedCheckStateIT extends AbstractCheckersTest {
+ // Tests against Gerrit Java API once it exists.
+ private Checks checks;
+ private PatchSet.Id patchSetId;
+
+ @Before
+ public void setUp() throws Exception {
+ checks = plugin.getHttpInjector().getInstance(Checks.class);
+
+ patchSetId = createChange().getPatchSetId();
+ }
+
+ @Test
+ public void returnsNotRelevantWhenNoCheck() throws Exception {
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.NOT_RELEVANT);
+ }
+
+ @Test
+ public void returnsSuccessfulWhenForSuccessfulCheckWhoseCheckerIsDisabled() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().status(CheckerStatus.DISABLED).create();
+ setCheckSuccessful(checkerUuid);
+
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.SUCCESSFUL);
+ }
+
+ @Test
+ public void returnsWarningForFailedCheckWhoseCheckerIsNotRelevant() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().query("status:merged").create();
+ setCheckState(checkerUuid, CheckState.FAILED);
+
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.WARNING);
+ }
+
+ @Test
+ public void returnsFailedWhenAnyRequiredCheckerFailed() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ setCheckSuccessful(checkerUuid);
+ CheckerUuid otherCheckerUuid = newRequiredChecker().create();
+ setCheckState(otherCheckerUuid, CheckState.FAILED);
+
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.FAILED);
+ }
+
+ @Test
+ public void returnsSuccessfulWhenAllRequiredCheckersSucceeded() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ setCheckSuccessful(checkerUuid);
+ CheckerUuid otherCheckerUuid = newRequiredChecker().create();
+ setCheckSuccessful(otherCheckerUuid);
+
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.SUCCESSFUL);
+ }
+
+ @Test
+ public void returnsInProgressWithOnlyBackFilledCheck() throws Exception {
+ newRequiredChecker().create();
+ // No check state is created.
+
+ CombinedCheckState combinedCheckState = checks.getCombinedCheckState(project, patchSetId);
+
+ assertThat(combinedCheckState).isEqualTo(CombinedCheckState.IN_PROGRESS);
+ }
+
+ private TestCheckerCreation.Builder newRequiredChecker() {
+ return checkerOperations
+ .newChecker()
+ .repository(project)
+ .status(CheckerStatus.ENABLED)
+ .blockingConditions(BlockingCondition.STATE_NOT_PASSING);
+ }
+
+ private void setCheckSuccessful(CheckerUuid checkerUuid) {
+ setCheckState(checkerUuid, CheckState.SUCCESSFUL);
+ }
+
+ private void setCheckState(CheckerUuid checkerUuid, CheckState checkState) {
+ CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+ checkOperations.newCheck(checkKey).setState(checkState).upsert();
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/rules/BUILD b/javatests/com/google/gerrit/plugins/checks/acceptance/rules/BUILD
new file mode 100644
index 0000000..61d2a35
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/rules/BUILD
@@ -0,0 +1,16 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "checks_submit_rules",
+ labels = [
+ "checks_submit_rules",
+ ],
+ deps = [
+ "//java/com/google/gerrit/git/testing",
+ "//java/com/google/gerrit/server/util/time",
+ "//plugins/checks:checks__plugin",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance",
+ "//plugins/checks/java/com/google/gerrit/plugins/checks/acceptance/testsuite",
+ ],
+)
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/rules/ChecksSubmitRuleIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/rules/ChecksSubmitRuleIT.java
new file mode 100644
index 0000000..32b8b03
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/rules/ChecksSubmitRuleIT.java
@@ -0,0 +1,162 @@
+// Copyright (C) 2019 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.plugins.checks.acceptance.rules;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckerUuid;
+import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import com.google.gerrit.plugins.checks.acceptance.testsuite.TestCheckerCreation;
+import com.google.gerrit.plugins.checks.api.BlockingCondition;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CheckerStatus;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.inject.Inject;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * TODO(xchangcheng): add more tests after figuring out the expecting behavior of {@code
+ * CombinedCheckState}.
+ */
+public class ChecksSubmitRuleIT extends AbstractCheckersTest {
+ private static final SubmitRequirementInfo SUBMIT_REQUIREMENT_INFO =
+ new SubmitRequirementInfo(
+ "NOT_READY", "All required checks must pass", "checks_pass", ImmutableMap.of());
+
+ @Inject private AllProjectsName allProjects;
+ private String testChangeId;
+ private PatchSet.Id testPatchSetId;
+
+ @Before
+ public void setUp() throws Exception {
+ PushOneCommit.Result result = createChange();
+ testPatchSetId = result.getPatchSetId();
+ testChangeId = result.getChangeId();
+
+ // Approves "Code-Review" label so that the change only needs to meet the submit requirements
+ // about checks.
+ approve(testChangeId);
+ }
+
+ @Test
+ public void nonApplicableCheckerNotBlockingSubmit() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ postCheckResult(checkerUuid, CheckState.FAILED);
+ // Updates the checker so that it isn't applicable to the change any more.
+ checkerOperations.checker(checkerUuid).forUpdate().repository(allProjects).update();
+
+ ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+
+ assertThat(changeInfo.submittable).isTrue();
+ assertThat(changeInfo.requirements).isEmpty();
+ }
+
+ @Test
+ public void disabledCheckerDoesNotBlockingSubmit() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ postCheckResult(checkerUuid, CheckState.FAILED);
+ checkerOperations.checker(checkerUuid).forUpdate().disable().update();
+
+ ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+
+ assertThat(changeInfo.submittable).isTrue();
+ assertThat(changeInfo.requirements).isEmpty();
+ }
+
+ // @Test
+ // public void enabledCheckerNotBlockingSubmitIfNotRequired() throws Exception {
+ // CheckerUuid checkerUuid = newRequiredChecker().create();
+ // postCheckResult(checkerUuid, CheckState.FAILED);
+ // checkerOperations
+ // .checker(checkerUuid)
+ // .forUpdate()
+ // .blockingConditions(ImmutableSortedSet.of())
+ // .update();
+ //
+ // ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+ //
+ // assertThat(changeInfo.submittable).isTrue();
+ // }
+
+ @Test
+ public void enabledCheckerNotBlockingSubmitIfNotInBlockingState() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ postCheckResult(checkerUuid, CheckState.SUCCESSFUL);
+
+ ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+
+ assertThat(changeInfo.submittable).isTrue();
+ assertThat(changeInfo.requirements).isEmpty();
+ }
+
+ @Test
+ public void enabledCheckerBlockingSubmitIfInBlockingState() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ postCheckResult(checkerUuid, CheckState.FAILED);
+
+ ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+
+ assertThat(changeInfo.submittable).isFalse();
+ assertThat(changeInfo.requirements).containsExactly(SUBMIT_REQUIREMENT_INFO);
+ }
+
+ @Test
+ public void multipleCheckerBlockingSubmit() throws Exception {
+ CheckerUuid checkerUuid = newRequiredChecker().create();
+ // Two enabled and required checkers. They are blocking if any of them isn't passing.
+ CheckerUuid testCheckerUuid2 = newRequiredChecker().create();
+ postCheckResult(checkerUuid, CheckState.SUCCESSFUL);
+ postCheckResult(testCheckerUuid2, CheckState.FAILED);
+
+ ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+
+ assertThat(changeInfo.submittable).isFalse();
+ assertThat(changeInfo.requirements).containsExactly(SUBMIT_REQUIREMENT_INFO);
+ }
+
+ // @Test
+ // public void multipleCheckerNotBlockingSubmit() throws Exception {
+ // CheckerUuid checkerUuid = newRequiredChecker().create();
+ // // Two enabled checkers. The one failed doesn't block because it's not required.
+ // CheckerUuid testCheckerUuidNotRequired =
+ // newRequiredChecker().blockingConditions(ImmutableSortedSet.of()).create();
+ // postCheckResult(testCheckerUuidNotRequired, CheckState.FAILED);
+ // postCheckResult(checkerUuid, CheckState.SUCCESSFUL);
+ //
+ // ChangeInfo changeInfo = gApi.changes().id(testChangeId).get();
+ //
+ // assertThat(changeInfo.submittable).isTrue();
+ // }
+
+ private TestCheckerCreation.Builder newRequiredChecker() {
+ return checkerOperations
+ .newChecker()
+ .repository(project)
+ .status(CheckerStatus.ENABLED)
+ .blockingConditions(BlockingCondition.STATE_NOT_PASSING);
+ }
+
+ private void postCheckResult(CheckerUuid checkerUuid, CheckState checkState) {
+ CheckKey checkKey = CheckKey.create(project, testPatchSetId, checkerUuid);
+ checkOperations.newCheck(checkKey).setState(checkState).upsert();
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/api/BlockingConditionTest.java b/javatests/com/google/gerrit/plugins/checks/api/BlockingConditionTest.java
new file mode 100644
index 0000000..a0e1b22
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/api/BlockingConditionTest.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 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.plugins.checks.api;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.testing.GerritBaseTests;
+import org.junit.Test;
+
+public class BlockingConditionTest extends GerritBaseTests {
+ @Test
+ public void isRequired() {
+ assertThat(BlockingCondition.isRequired(ImmutableSet.of())).isFalse();
+ assertThat(BlockingCondition.isRequired(ImmutableSet.of(BlockingCondition.STATE_NOT_PASSING)))
+ .isTrue();
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..b7be92e
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,2 @@
+The @PLUGIN@ plugin provides a REST API and UI extensions for integrating CI
+systems with Gerrit.
diff --git a/src/main/resources/Documentation/rest-api-changes.md b/src/main/resources/Documentation/rest-api-changes.md
new file mode 100644
index 0000000..904fc16
--- /dev/null
+++ b/src/main/resources/Documentation/rest-api-changes.md
@@ -0,0 +1,43 @@
+# /changes REST API
+
+This page describes additions to the Gerrit change-related REST endpoints that
+are added by the @PLUGIN@ plugin.
+
+Please also take note of the general information on the
+[changes REST API](../../../Documentation/rest-api-changes.html).
+
+### <a id="get-change"> Get Change options
+_'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)?@PLUGIN@--combined'_
+
+Adding the query parameter `@PLUGIN@--combined` adds a `CheckChangeInfo` entity
+to the `plugins` list in
+[`ChangeInfo`](../../../Documentation/rest-api-changes.html#change-info). All
+fields reflect up-to-date information read from primary storage, not a secondary
+index.
+
+### <a id="check-change-info"> CheckChangeInfo
+
+The `CheckChangeInfo` describes check information on a change.
+
+| Field Name | Description |
+| ---------------------- | ----------- |
+| `combined_check_state` | The [combined state](#combined-check-state) of all checks on the change.
+
+### <a id="combined-check-state"> CombinedCheckState (enum)
+
+The `CheckState` enum can have the following values:
+
+* `FAILED`: At least one required check failed; other checks may have passed, or
+ still be running.
+
+* `WARNING`: All relevant checks terminated, and at least one optional check
+ failed, but no required checks failed.
+
+* `IN_PROGRESS`: At least one relevant check is in a non-terminated
+ [state](rest-api-checks.md#check-state) (`NOT_STARTED`, `SCHEDULED`,
+ `RUNNING`), and no required checks failed. Some optional checks may have
+ failed.
+
+* `SUCCESSFUL`: All relevant checks terminated successfully.
+
+* `NOT_RELEVANT:` No checks are relevant to this change.
diff --git a/src/main/resources/Documentation/rest-api-pending-checks.md b/src/main/resources/Documentation/rest-api-pending-checks.md
index 39e2a1a..466dff3 100644
--- a/src/main/resources/Documentation/rest-api-pending-checks.md
+++ b/src/main/resources/Documentation/rest-api-pending-checks.md
@@ -11,7 +11,7 @@
### <a id="get-checker"> List Pending Checks
_'GET /checks.pending/'_
-Lists pending checks for a checker or for all checkers of a scheme.
+Lists pending checks for a checker.
Checks are pending if they are in a non-final state and the external
checker system intends to post further updates on them.
@@ -23,19 +23,17 @@
Request parameters:
* <a id="checker-param"> `checker`: the UUID of the checker for which
- pending checks should be listed (optional, if not specified `scheme`
- must be set)
-* <a id="scheme-param"> `scheme`: the scheme of the checkers for which
- pending checks should be listed (optional, if not specified `checker`
- must be set)
+ pending checks should be listed (required)
* <a id="state-param"> `state`: state that should be considered as
pending (optional, by default the state `NOT_STARTED` is assumed,
this option may be specified multiple times to request checks
matching any of several states)
-Note that only users with the [Administrate
-Checkers](access-control.md#capability_administrateCheckers) global capability
-are permitted to list pending checks.
+This REST endpoint only returns pending checks for current patch sets.
+
+Note that all users are allowed to list pending checks but the result
+includes only checks on changes that are visible to the calling user.
+This means pending checks for non-visible changes are filtered out.
#### Request by checker
@@ -81,56 +79,6 @@
]
```
-#### Request by checker scheme
-
-```
- GET /checks.pending/?scheme=test&state=NOT_STARTED&state=SCHEDULED HTTP/1.0
-```
-
-As response a list of [PendingChecksInfo](#pending-checks-info)
-entities is returned that describes the pending checks.
-
-#### Response by checker scheme
-
-```
- HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
- )]}'
- [
- {
- "patch_set": {
- "project": "test-project",
- "change_number": 1,
- "patch_set_id": 1,
- }
- "pending_checks": {
- "test:my-checker": {
- "state": "NOT_STARTED",
- },
- "test:my-other-checker": {
- "state": "SCHEDULED",
- }
- }
- },
- {
- "patch_set": {
- "project": "test-project",
- "change_number": 5,
- "patch_set_id": 2,
- }
- "pending_checks": {
- "test:my-checker": {
- "state": "NOT_STARTED",
- },
- "test:my-other-checker": {
- "state": "NOT_STARTED",
- }
- }
- }
- ]
-```
-
## <a id="json-entities"> JSON Entities
### <a id="checkable-patch-set-info"> CheckablePatchSetInfo