Merge changes If7a8f1aa,Ida362917
* changes:
Implement List Pending Checks REST endpoint
Move relevant checker logic to "Checker"
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
index 8bf6709..333e4fe 100644
--- a/java/com/google/gerrit/plugins/checks/Checker.java
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -15,17 +15,35 @@
package com.google.gerrit.plugins.checks;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Throwables;
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.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+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 +144,91 @@
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<InternalChangeQuery> changeQueryProvider)
+ throws ConfigInvalidException, OrmException {
+ return executeIndexQueryWithRetry(
+ retryHelper, changeQueryProvider, 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 InternalChangeQuery.
+ private List<ChangeData> executeIndexQueryWithRetry(
+ RetryHelper retryHelper,
+ Provider<InternalChangeQuery> changeQueryProvider,
+ Predicate<ChangeData> predicate)
+ throws OrmException {
+ try {
+ return retryHelper.execute(
+ ActionType.INDEX_QUERY,
+ () -> changeQueryProvider.get().query(predicate),
+ OrmException.class::isInstance);
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, OrmException.class);
+ throw new OrmException(e);
+ }
+ }
+
/** A builder for an {@link Checker}. */
@AutoValue.Builder
public abstract static class Builder {
diff --git a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
index 9bc5bb6..51e26d5 100644
--- a/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/ListPendingChecks.java
@@ -14,23 +14,47 @@
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.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+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.Checks.GetCheckOptions;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+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<InternalChangeQuery> changeQueryProvider;
private CheckerUuid checkerUuid;
private List<CheckState> states = new ArrayList<>(CheckState.values().length);
@@ -51,14 +75,26 @@
@Inject
public ListPendingChecks(
- PermissionBackend permissionBackend, AdministrateCheckersPermission permission) {
+ PermissionBackend permissionBackend,
+ AdministrateCheckersPermission permission,
+ Checkers checkers,
+ Checks checks,
+ RetryHelper retryHelper,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
+ Provider<InternalChangeQuery> changeQueryProvider) {
this.permissionBackend = permissionBackend;
this.permission = permission;
+ this.checkers = checkers;
+ this.checks = checks;
+ this.retryHelper = retryHelper;
+ this.queryBuilderProvider = queryBuilderProvider;
+ this.changeQueryProvider = changeQueryProvider;
}
@Override
public List<PendingChecksInfo> apply(TopLevelResource resource)
- throws RestApiException, PermissionBackendException {
+ throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+ OrmException {
permissionBackend.currentUser().check(permission);
if (states.isEmpty()) {
@@ -70,7 +106,67 @@
throw new BadRequestException("checker UUID is required");
}
- // TODO(ekempin): Implement this REST endpoint
- throw new MethodNotAllowedException("not implemented");
+ Checker checker =
+ checkers
+ .getChecker(checkerUuid)
+ .orElseThrow(
+ () ->
+ new UnprocessableEntityException(
+ String.format("checker %s not found", checkerUuid)));
+
+ if (checker.getStatus() == CheckerStatus.DISABLED) {
+ return ImmutableList.of();
+ }
+
+ // 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(), changeQueryProvider);
+ 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/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/db/CheckBackfiller.java b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
index f8c18e4..33119e5 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
@@ -16,9 +16,6 @@
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;
@@ -28,12 +25,10 @@
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;
@@ -81,7 +76,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));
}
@@ -107,7 +102,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()));
@@ -124,41 +119,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/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/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/ListPendingChecksIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
index 2dd95f1..d567cd0 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ListPendingChecksIT.java
@@ -15,19 +15,52 @@
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 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.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.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 specifyingCheckerUuidIsRequired() throws Exception {
// The extension API doesn't allow to not specify a checker UUID. Call the endpoint over REST to
@@ -54,9 +87,238 @@
}
@Test
- public void listPendingChecksForCheckerNotImplemented() throws Exception {
- exception.expect(MethodNotAllowedException.class);
- exception.expectMessage("not implemented");
- pendingChecksApi.list("foo:bar");
+ public void cannotListPendingChecksForNonExistingChecker() throws Exception {
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("checker non:existing not found");
+ pendingChecksApi.list("non:existing");
+ }
+
+ @Test
+ 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);
}
}
diff --git a/src/main/resources/Documentation/rest-api-pending-checks.md b/src/main/resources/Documentation/rest-api-pending-checks.md
index 62e64c8..2333afc 100644
--- a/src/main/resources/Documentation/rest-api-pending-checks.md
+++ b/src/main/resources/Documentation/rest-api-pending-checks.md
@@ -29,6 +29,8 @@
this option may be specified multiple times to request checks
matching any of several states)
+This REST endpoint only returns pending checks for current patch sets.
+
Note that only users with the [Administrate
Checkers](access-control.md#capability_administrateCheckers) global capability
are permitted to list pending checks.