Add #getCombinedCheckState to the Check interface

This method is useful for callers who want to know the
combined check state of a patch set. For example,
in the follow-up change, the "ChecksSubmitRule" would
simply call this API to get the combined check state
of the latest patch set and then use that state to
decide whether a change is submittable or not.

Basic tests are added to verify the bahavior of this
API, more specific behaviors are covered by existing
"CombinedCheckStateTest".

Change-Id: I341dfdab8beaccd28d567d31196ab9a542661d24
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
index 333e4fe..5bae4c7 100644
--- a/java/com/google/gerrit/plugins/checks/Checker.java
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -16,6 +16,7 @@
 
 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;
@@ -229,6 +230,24 @@
     }
   }
 
+  /**
+   * 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 768908b..7d6cd03 100644
--- a/java/com/google/gerrit/plugins/checks/Checks.java
+++ b/java/com/google/gerrit/plugins/checks/Checks.java
@@ -16,6 +16,7 @@
 
 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;
@@ -61,6 +62,18 @@
   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() {
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
index 33119e5..dfd3892 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.plugins.checks.db;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.Checker;
@@ -25,9 +24,7 @@
 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.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.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -40,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.
@@ -85,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.
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
index be242f7..c01b359 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
@@ -18,6 +18,8 @@
 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;
@@ -25,39 +27,50 @@
 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,
+      ChangeData.Factory changeDataFactory,
       CheckNotes.Factory checkNotesFactory,
       Checkers checkers,
-      CheckBackfiller checkBackfiller) {
-    this.changeNotesFactory = changeNotesFactory;
-    this.psUtil = psUtil;
+      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
@@ -78,10 +91,10 @@
             .findAny();
 
     if (!result.isPresent() && options.backfillChecks()) {
-      ChangeNotes notes =
-          changeNotesFactory.create(checkKey.project(), checkKey.patchSet().getParentKey());
+      ChangeData changeData =
+          changeDataFactory.create(checkKey.project(), checkKey.patchSet().getParentKey());
       return checkBackfiller.getBackfilledCheckForRelevantChecker(
-          checkKey.checkerUuid(), notes, checkKey.patchSet());
+          checkKey.checkerUuid(), changeData, checkKey.patchSet());
     }
 
     return result;
@@ -91,9 +104,9 @@
       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();
 
     ImmutableList<Check> existingChecks =
@@ -109,12 +122,53 @@
     ImmutableList<Checker> checkersForBackfiller =
         getCheckersForBackfiller(projectName, existingChecks);
     ImmutableList<Check> backfilledChecks =
-        checkBackfiller.getBackfilledChecksForRelevantCheckers(checkersForBackfiller, notes, psId);
+        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 =
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/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();
+  }
+}