Merge "Add backend support for re-running checks."
diff --git a/java/com/google/gerrit/plugins/checks/api/ApiModule.java b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
index ad1e7e3..88fb5cf 100644
--- a/java/com/google/gerrit/plugins/checks/api/ApiModule.java
+++ b/java/com/google/gerrit/plugins/checks/api/ApiModule.java
@@ -50,7 +50,7 @@
             postOnCollection(CHECK_KIND).to(PostCheck.class);
             get(CHECK_KIND).to(GetCheck.class);
             post(CHECK_KIND).to(UpdateCheck.class);
-
+            post(CHECK_KIND, "rerun").to(RerunCheck.class);
             DynamicMap.mapOf(binder(), PENDING_CHECK_KIND);
           }
         });
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckApi.java b/java/com/google/gerrit/plugins/checks/api/CheckApi.java
index d137645..14acb41 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckApi.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckApi.java
@@ -26,6 +26,8 @@
   /** Updates a check and returns the {@link CheckInfo} for the updated resource. */
   CheckInfo update(CheckInput input) throws RestApiException;
 
+  /** Reruns the check and returns the {@link CheckInfo} for the updated check. */
+  CheckInfo rerun() throws RestApiException;
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -40,5 +42,10 @@
     public CheckInfo update(CheckInput input) throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public CheckInfo rerun() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java b/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
index 5cd20f6..3be0a74 100644
--- a/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
+++ b/java/com/google/gerrit/plugins/checks/api/CheckApiImpl.java
@@ -30,12 +30,18 @@
   private final GetCheck getCheck;
   private final UpdateCheck updateCheck;
   private final CheckResource checkResource;
+  private final RerunCheck rerunCheck;
 
   @Inject
-  CheckApiImpl(GetCheck getCheck, UpdateCheck updateCheck, @Assisted CheckResource checkResource) {
+  CheckApiImpl(
+      GetCheck getCheck,
+      UpdateCheck updateCheck,
+      @Assisted CheckResource checkResource,
+      RerunCheck rerunCheck) {
     this.getCheck = getCheck;
     this.updateCheck = updateCheck;
     this.checkResource = checkResource;
+    this.rerunCheck = rerunCheck;
   }
 
   @Override
@@ -56,4 +62,13 @@
       throw asRestApiException("Cannot update check", e);
     }
   }
+
+  @Override
+  public CheckInfo rerun() throws RestApiException {
+    try {
+      return rerunCheck.apply(checkResource, null).value();
+    } catch (Exception e) {
+      throw asRestApiException("Cannot rerun check", e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/RerunCheck.java b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
new file mode 100644
index 0000000..4c6b0a4
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/api/RerunCheck.java
@@ -0,0 +1,119 @@
+// 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.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.plugins.checks.AdministrateCheckersPermission;
+import com.google.gerrit.plugins.checks.Check;
+import com.google.gerrit.plugins.checks.CheckJson;
+import com.google.gerrit.plugins.checks.CheckKey;
+import com.google.gerrit.plugins.checks.CheckUpdate;
+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.ChecksUpdate;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import javax.inject.Provider;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class RerunCheck implements RestModifyView<CheckResource, Input> {
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final AdministrateCheckersPermission permission;
+  private final Checks checks;
+  private final Provider<ChecksUpdate> checksUpdate;
+  private final CheckJson.Factory checkJsonFactory;
+  private final Checkers checkers;
+
+  @Inject
+  RerunCheck(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      AdministrateCheckersPermission permission,
+      Checks checks,
+      @UserInitiated Provider<ChecksUpdate> checksUpdate,
+      CheckJson.Factory checkJsonFactory,
+      Checkers checkers) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.permission = permission;
+    this.checks = checks;
+    this.checksUpdate = checksUpdate;
+    this.checkJsonFactory = checkJsonFactory;
+    this.checkers = checkers;
+  }
+
+  @Override
+  public Response<CheckInfo> apply(CheckResource checkResource, Input input)
+      throws RestApiException, IOException, PermissionBackendException, ConfigInvalidException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    permissionBackend.currentUser().check(permission);
+    if (checkResource.getRevisionResource().getEdit().isPresent()) {
+      throw new ResourceConflictException("checks are not supported on a change edit");
+    }
+    CheckKey key =
+        CheckKey.create(
+            checkResource.getRevisionResource().getProject(),
+            checkResource.getRevisionResource().getPatchSet().id(),
+            checkResource.getCheckerUuid());
+    Optional<Check> check = checks.getCheck(key, GetCheckOptions.defaults());
+    CheckerUuid checkerUuid = checkResource.getCheckerUuid();
+    Check updatedCheck;
+    if (!check.isPresent()) {
+      Checker checker =
+          checkers
+              .getChecker(checkerUuid)
+              .orElseThrow(
+                  () ->
+                      new ResourceNotFoundException(
+                          String.format("checker %s not found", checkerUuid)));
+      // This error should not be thrown since this case is filtered before reaching this code.
+      // Also return a backfilled check for checkers that do not apply to the change.
+      updatedCheck =
+          Check.newBackfilledCheck(
+              checkResource.getRevisionResource().getProject(),
+              checkResource.getRevisionResource().getPatchSet(),
+              checker);
+    } else {
+      CheckUpdate.Builder builder = CheckUpdate.builder();
+      builder
+          .setState(CheckState.NOT_STARTED)
+          .unsetFinished()
+          .unsetStarted()
+          .setMessage("")
+          .setUrl("");
+      updatedCheck = checksUpdate.get().updateCheck(key, builder.build());
+    }
+    return Response.ok(checkJsonFactory.noOptions().format(updatedCheck));
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
index 557aab0..03a9e7d 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/ChecksRestApiBindingsIT.java
@@ -47,7 +47,8 @@
   private static final ImmutableList<RestCall> SCOPED_CHECK_ENDPOINTS =
       ImmutableList.of(
           RestCall.get("/changes/%s/revisions/%s/checks~checks/%s"),
-          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s"));
+          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s"),
+          RestCall.post("/changes/%s/revisions/%s/checks~checks/%s/rerun"));
 
   @Test
   public void rootEndpoints() throws Exception {
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java
new file mode 100644
index 0000000..9cc539f
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/RerunCheckIT.java
@@ -0,0 +1,150 @@
+// 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.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+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.CheckInfo;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.TestTimeUtil;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RerunCheckIT extends AbstractCheckersTest {
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  private PatchSet.Id patchSetId;
+  private CheckKey checkKey;
+
+  @Before
+  public void setUp() throws Exception {
+    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+    TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
+
+    patchSetId = createChange().getPatchSetId();
+
+    CheckerUuid checkerUuid = checkerOperations.newChecker().repository(project).create();
+    checkKey = CheckKey.create(project, patchSetId, checkerUuid);
+  }
+
+  @After
+  public void resetTime() {
+    TestTimeUtil.useSystemTime();
+  }
+
+  @Test
+  public void rerunResetsCheckInfo() throws Exception {
+    checkOperations
+        .newCheck(checkKey)
+        .state(CheckState.FAILED)
+        .started(TimeUtil.nowTs())
+        .finished(TimeUtil.nowTs())
+        .message("message")
+        .url("url.com")
+        .upsert();
+    Timestamp created = checkOperations.check(checkKey).get().created();
+    Timestamp updated = checkOperations.check(checkKey).get().updated();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(info.message).isEqualTo(null);
+    assertThat(info.url).isEqualTo(null);
+    assertThat(info.started).isEqualTo(null);
+    assertThat(info.finished).isEqualTo(null);
+    assertThat(info.created).isEqualTo(created);
+    assertThat(info.updated).isGreaterThan(updated);
+  }
+
+  @Test
+  public void rerunNotStartedCheck() throws Exception {
+    checkOperations.newCheck(checkKey).state(CheckState.NOT_STARTED).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+  }
+
+  @Test
+  public void rerunFinishedCheck() throws Exception {
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(info.updated).isGreaterThan(info.created);
+  }
+
+  @Test
+  public void rerunCheckNotExistingButBackfilled() throws Exception {
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+    assertThat(checkOperations.check(checkKey).exists()).isFalse();
+  }
+
+  @Test
+  public void rerunExistingCheckWithCheckerNotAppliedToChange() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    checkerOperations.checker(checkKey.checkerUuid()).forUpdate().repository(otherProject).update();
+    checkOperations.newCheck(checkKey).upsert();
+    CheckInfo info = checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun();
+    assertThat(info.state).isEqualTo(CheckState.NOT_STARTED);
+  }
+
+  @Test
+  public void rerunNonExistingCheckWithCheckerNotAppliedToChange() throws Exception {
+    Project.NameKey otherProject = projectOperations.newProject().create();
+    checkerOperations.checker(checkKey.checkerUuid()).forUpdate().repository(otherProject).update();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(checkOperations.check(checkKey).exists()).isFalse();
+  }
+
+  @Test
+  public void cannotUpdateCheckWithoutAdministrateCheckers() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(thrown).hasMessageThat().contains("not permitted");
+  }
+
+  @Test
+  public void cannotUpdateCheckAnonymously() throws Exception {
+    requestScopeOperations.setApiUserAnonymous();
+    checkOperations.newCheck(checkKey).state(CheckState.SUCCESSFUL).upsert();
+
+    AuthException thrown =
+        assertThrows(
+            AuthException.class,
+            () -> checksApiFactory.revision(patchSetId).id(checkKey.checkerUuid()).rerun());
+    assertThat(thrown).hasMessageThat().contains("Authentication required");
+  }
+}
diff --git a/resources/Documentation/rest-api-checks.md b/resources/Documentation/rest-api-checks.md
index 048c868..4395644 100644
--- a/resources/Documentation/rest-api-checks.md
+++ b/resources/Documentation/rest-api-checks.md
@@ -157,6 +157,17 @@
 the URL, it must either match the value provided in the request body via
 [CheckInput](#check-input) or the value in the request body is omitted.
 
+### <a id="rerun-check"> Rerun Check
+
+_'POST /changes/1/revisions/1/checks/test:my-checker/rerun'_
+
+Reruns a check. As response the [CheckInfo](#check-info) entity is returned that
+describes the created check.
+
+
+This REST endpoint supports rerunning a check. It also resets all relevant check
+fields such as `message`, `url`, `started` and `finished`.
+
 ## <a id="json-entities"> JSON Entities
 
 ### <a id="check-info"> CheckInfo