Merge "Send email notification if combined check state is updated"
diff --git a/java/com/google/gerrit/plugins/checks/ChecksStorageUpdate.java b/java/com/google/gerrit/plugins/checks/ChecksStorageUpdate.java
new file mode 100644
index 0000000..bdf8693
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/ChecksStorageUpdate.java
@@ -0,0 +1,47 @@
+// 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 com.google.gerrit.exceptions.DuplicateKeyException;
+import java.io.IOException;
+
+/**
+ * API for updating checks in the storage backend.
+ *
+ * <p>To be implemented by check storage backends.
+ */
+public interface ChecksStorageUpdate {
+ /**
+ * Creates a new check in the storage backend.
+ *
+ * @param key the key of the check that should be created
+ * @param checkUpdate the update describing the properties of the new check
+ * @return the newly created check
+ * @throws DuplicateKeyException thrown if a check with the given key already exists
+ * @throws IOException thrown in case of an I/O error
+ */
+ public Check createCheck(CheckKey key, CheckUpdate checkUpdate)
+ throws DuplicateKeyException, IOException;
+
+ /**
+ * Updates an existing check in the storage backend.
+ *
+ * @param key the key of the check that should be updated
+ * @param checkUpdate the update describing the check properties that should be updated
+ * @return the updated check
+ * @throws IOException thrown in case of an I/O error
+ */
+ public Check updateCheck(CheckKey key, CheckUpdate checkUpdate) throws IOException;
+}
diff --git a/java/com/google/gerrit/plugins/checks/ChecksUpdate.java b/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
index ba58afa..f29d079 100644
--- a/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/ChecksUpdate.java
@@ -14,12 +14,127 @@
package com.google.gerrit.plugins.checks;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.plugins.checks.email.CombinedCheckStateUpdatedSender;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.util.Optional;
-public interface ChecksUpdate {
- Check createCheck(CheckKey key, CheckUpdate checkUpdate)
- throws DuplicateKeyException, IOException;
+/**
+ * API to update checks.
+ *
+ * <p>Delegates the persistence of checks to the storage layer (see {@link ChecksStorageUpdate}).
+ *
+ * <p>This class contains additional business logic for updating checks which is independent of the
+ * used storage layer (e.g. sending email notifications).
+ */
+public class ChecksUpdate {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- Check updateCheck(CheckKey key, CheckUpdate checkUpdate) throws IOException;
+ interface Factory {
+ ChecksUpdate create(IdentifiedUser currentUser);
+
+ ChecksUpdate createWithServerIdent();
+ }
+
+ private final CombinedCheckStateCache combinedCheckStateCache;
+ private final CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory;
+ private final ChangeNotes.Factory notesFactory;
+ private final PatchSetUtil psUtil;
+ private final Optional<IdentifiedUser> currentUser;
+ private final ChecksStorageUpdate checksStorageUpdate;
+
+ @AssistedInject
+ ChecksUpdate(
+ @UserInitiated ChecksStorageUpdate checksStorageUpdate,
+ CombinedCheckStateCache combinedCheckStateCache,
+ CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory,
+ ChangeNotes.Factory notesFactory,
+ PatchSetUtil psUtil,
+ @Assisted IdentifiedUser currentUser) {
+ this.combinedCheckStateCache = combinedCheckStateCache;
+ this.combinedCheckStateUpdatedSenderFactory = combinedCheckStateUpdatedSenderFactory;
+ this.notesFactory = notesFactory;
+ this.psUtil = psUtil;
+ this.currentUser = Optional.of(currentUser);
+ this.checksStorageUpdate = checksStorageUpdate;
+ }
+
+ @AssistedInject
+ ChecksUpdate(
+ @ServerInitiated ChecksStorageUpdate checksStorageUpdate,
+ CombinedCheckStateCache combinedCheckStateCache,
+ CombinedCheckStateUpdatedSender.Factory combinedCheckStateUpdatedSenderFactory,
+ ChangeNotes.Factory notesFactory,
+ PatchSetUtil psUtil) {
+ this.combinedCheckStateCache = combinedCheckStateCache;
+ this.combinedCheckStateUpdatedSenderFactory = combinedCheckStateUpdatedSenderFactory;
+ this.notesFactory = notesFactory;
+ this.psUtil = psUtil;
+ this.currentUser = Optional.empty();
+ this.checksStorageUpdate = checksStorageUpdate;
+ }
+
+ public Check createCheck(CheckKey key, CheckUpdate checkUpdate)
+ throws DuplicateKeyException, IOException {
+ CombinedCheckState oldCombinedCheckState =
+ combinedCheckStateCache.get(key.repository(), key.patchSet());
+
+ Check check = checksStorageUpdate.createCheck(key, checkUpdate);
+
+ CombinedCheckState newCombinedCheckState =
+ combinedCheckStateCache.get(key.repository(), key.patchSet());
+ if (oldCombinedCheckState != newCombinedCheckState) {
+ sendEmail(key, newCombinedCheckState);
+ }
+
+ return check;
+ }
+
+ public Check updateCheck(CheckKey key, CheckUpdate checkUpdate) throws IOException {
+ CombinedCheckState oldCombinedCheckState =
+ combinedCheckStateCache.get(key.repository(), key.patchSet());
+
+ Check check = checksStorageUpdate.updateCheck(key, checkUpdate);
+
+ CombinedCheckState newCombinedCheckState =
+ combinedCheckStateCache.get(key.repository(), key.patchSet());
+ if (oldCombinedCheckState != newCombinedCheckState) {
+ sendEmail(key, newCombinedCheckState);
+ }
+
+ return check;
+ }
+
+ private void sendEmail(CheckKey checkKey, CombinedCheckState combinedCheckState) {
+ try {
+ CombinedCheckStateUpdatedSender sender =
+ combinedCheckStateUpdatedSenderFactory.create(
+ checkKey.repository(), checkKey.patchSet().changeId());
+
+ if (currentUser.isPresent()) {
+ sender.setFrom(currentUser.get().getAccountId());
+ }
+
+ ChangeNotes changeNotes =
+ notesFactory.create(checkKey.repository(), checkKey.patchSet().changeId());
+ PatchSet patchSet = psUtil.get(changeNotes, checkKey.patchSet());
+ sender.setPatchSet(patchSet);
+
+ sender.setCombinedCheckState(combinedCheckState);
+ sender.send();
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log(
+ "Cannot email update for change %s", checkKey.patchSet().changeId());
+ }
+ }
}
diff --git a/java/com/google/gerrit/plugins/checks/Module.java b/java/com/google/gerrit/plugins/checks/Module.java
index 140497b..3431ec1 100644
--- a/java/com/google/gerrit/plugins/checks/Module.java
+++ b/java/com/google/gerrit/plugins/checks/Module.java
@@ -25,8 +25,12 @@
import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory.GetChangeOptions;
import com.google.gerrit.plugins.checks.api.ChangeCheckAttributeFactory.QueryChangesOptions;
import com.google.gerrit.plugins.checks.db.NoteDbCheckersModule;
+import com.google.gerrit.plugins.checks.email.ChecksEmailModule;
import com.google.gerrit.plugins.checks.rules.ChecksSubmitRule;
import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.UserInitiated;
import com.google.gerrit.server.change.ChangeAttributeFactory;
import com.google.gerrit.server.change.ChangeETagComputation;
import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -35,11 +39,13 @@
import com.google.gerrit.server.restapi.change.GetChange;
import com.google.gerrit.server.restapi.change.QueryChanges;
import com.google.gerrit.sshd.commands.Query;
+import com.google.inject.Provides;
public class Module extends FactoryModule {
@Override
protected void configure() {
factory(CheckJson.AssistedFactory.class);
+ factory(ChecksUpdate.Factory.class);
install(new NoteDbCheckersModule());
install(CombinedCheckStateCache.module());
@@ -74,5 +80,19 @@
install(new ApiModule());
install(new ChecksSubmitRule.Module());
+ install(new ChecksEmailModule());
+ }
+
+ @Provides
+ @ServerInitiated
+ ChecksUpdate provideServerInitiatedChecksUpdate(ChecksUpdate.Factory factory) {
+ return factory.createWithServerIdent();
+ }
+
+ @Provides
+ @UserInitiated
+ ChecksUpdate provideUserInitiatedChecksUpdate(
+ ChecksUpdate.Factory factory, IdentifiedUser currentUser) {
+ return factory.create(currentUser);
}
}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java
index 39e8d7c..efc278b 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbCheckersModule.java
@@ -18,7 +18,7 @@
import com.google.gerrit.plugins.checks.Checkers;
import com.google.gerrit.plugins.checks.CheckersUpdate;
import com.google.gerrit.plugins.checks.Checks;
-import com.google.gerrit.plugins.checks.ChecksUpdate;
+import com.google.gerrit.plugins.checks.ChecksStorageUpdate;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.UserInitiated;
@@ -51,13 +51,13 @@
@Provides
@ServerInitiated
- ChecksUpdate provideServerInitiatedChecksUpdate(NoteDbChecksUpdate.Factory factory) {
+ ChecksStorageUpdate provideServerInitiatedChecksUpdate(NoteDbChecksUpdate.Factory factory) {
return factory.createWithServerIdent();
}
@Provides
@UserInitiated
- ChecksUpdate provideUserInitiatedChecksUpdate(
+ ChecksStorageUpdate provideUserInitiatedChecksUpdate(
NoteDbChecksUpdate.Factory factory, IdentifiedUser currentUser) {
return factory.create(currentUser);
}
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
index 980a1fe..467a7bd 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecksUpdate.java
@@ -29,7 +29,7 @@
import com.google.gerrit.plugins.checks.CheckerRef;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.Checkers;
-import com.google.gerrit.plugins.checks.ChecksUpdate;
+import com.google.gerrit.plugins.checks.ChecksStorageUpdate;
import com.google.gerrit.plugins.checks.CombinedCheckStateCache;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -56,7 +56,7 @@
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
-public class NoteDbChecksUpdate implements ChecksUpdate {
+public class NoteDbChecksUpdate implements ChecksStorageUpdate {
interface Factory {
NoteDbChecksUpdate create(IdentifiedUser currentUser);
diff --git a/java/com/google/gerrit/plugins/checks/email/ChecksEmailModule.java b/java/com/google/gerrit/plugins/checks/email/ChecksEmailModule.java
new file mode 100644
index 0000000..3b4e016
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/email/ChecksEmailModule.java
@@ -0,0 +1,32 @@
+// 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.email;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
+
+public class ChecksEmailModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), MailSoyTemplateProvider.class)
+ .to(ChecksMailSoyTemplateProvider.class)
+ .in(SINGLETON);
+
+ factory(CombinedCheckStateUpdatedSender.Factory.class);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/email/ChecksMailSoyTemplateProvider.java b/java/com/google/gerrit/plugins/checks/email/ChecksMailSoyTemplateProvider.java
new file mode 100644
index 0000000..6bcfe08
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/email/ChecksMailSoyTemplateProvider.java
@@ -0,0 +1,33 @@
+// 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.email;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
+import com.google.inject.Singleton;
+import java.util.Set;
+
+@Singleton
+public class ChecksMailSoyTemplateProvider implements MailSoyTemplateProvider {
+ @Override
+ public String getPath() {
+ return "com/google/gerrit/plugins/checks/email/";
+ }
+
+ @Override
+ public Set<String> getFileNames() {
+ return ImmutableSet.of("CombinedCheckStateUpdated.soy", "CombinedCheckStateUpdatedHtml.soy");
+ }
+}
diff --git a/java/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedSender.java b/java/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedSender.java
new file mode 100644
index 0000000..560e97b
--- /dev/null
+++ b/java/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedSender.java
@@ -0,0 +1,78 @@
+// 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.email;
+
+import com.google.gerrit.exceptions.EmailException;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.mail.send.ChangeEmail;
+import com.google.gerrit.server.mail.send.EmailArguments;
+import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Send notice about an update of the combined check state of a change. */
+public class CombinedCheckStateUpdatedSender extends ReplyToChangeSender {
+ public interface Factory extends ReplyToChangeSender.Factory<CombinedCheckStateUpdatedSender> {
+ @Override
+ CombinedCheckStateUpdatedSender create(Project.NameKey project, Change.Id changeId);
+ }
+
+ private CombinedCheckState combinedCheckState;
+
+ @Inject
+ public CombinedCheckStateUpdatedSender(
+ EmailArguments args, @Assisted Project.NameKey project, @Assisted Change.Id changeId) {
+ super(args, "combinedCheckStateUpdate", ChangeEmail.newChangeData(args, project, changeId));
+ }
+
+ @Override
+ protected void init() throws EmailException {
+ super.init();
+
+ ccAllApprovals();
+ bccStarredBy();
+ includeWatchers(NotifyType.ALL_COMMENTS);
+ removeUsersThatIgnoredTheChange();
+ }
+
+ public void setCombinedCheckState(CombinedCheckState combinedCheckState) {
+ this.combinedCheckState = combinedCheckState;
+ }
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+
+ if (combinedCheckState != null) {
+ soyContext.put("combinedCheckState", combinedCheckState.name());
+ }
+ }
+
+ @Override
+ protected void formatChange() throws EmailException {
+ appendText(textTemplate("CombinedCheckStateUpdated"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("CombinedCheckStateUpdatedHtml"));
+ }
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java
index a00c2cd..7bbf3ee 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChangeCheckInfoIT.java
@@ -186,14 +186,14 @@
checksApiFactory.revision(psId).id(checkerUuid).update(checkInput);
// Incurs reload after updating check state.
- assertThat(cache.getStats()).since(start).hasHitCount(2);
+ assertThat(cache.getStats()).since(start).hasHitCount(4);
assertThat(cache.getStats()).since(start).hasMissCount(0);
assertThat(cache.getReloadCount(false) - startReloadsFalse).isEqualTo(0);
assertThat(cache.getReloadCount(true) - startReloadsTrue).isEqualTo(1);
assertThat(queryChangeCheckInfo(changeId))
.hasValue(new ChangeCheckInfo("checks", CombinedCheckState.WARNING));
- assertThat(cache.getStats()).since(start).hasHitCount(3);
+ assertThat(cache.getStats()).since(start).hasHitCount(5);
assertThat(cache.getStats()).since(start).hasMissCount(0);
assertThat(cache.getReloadCount(false) - startReloadsFalse).isEqualTo(0);
assertThat(cache.getReloadCount(true) - startReloadsTrue).isEqualTo(1);
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java
new file mode 100644
index 0000000..8723614
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/ChecksEmailIT.java
@@ -0,0 +1,241 @@
+// 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.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+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.CheckInput;
+import com.google.gerrit.plugins.checks.api.CheckState;
+import com.google.gerrit.plugins.checks.api.CombinedCheckState;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChecksEmailIT extends AbstractCheckersTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private GroupOperations groupOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ private TestAccount bot;
+ private CheckerUuid checkerUuid1;
+ private CheckerUuid checkerUuid2;
+ private TestAccount owner;
+ private TestAccount reviewer;
+ private TestAccount starrer;
+ private TestAccount watcher;
+ private PatchSet.Id patchSetId;
+
+ @Before
+ public void setup() throws Exception {
+ // Create a bot account, create a bots group and add the bot as member and allow the bots group
+ // to post checks.
+ bot = accountCreator.create("bot", "bot@test.com", "Bot");
+ AccountGroup.UUID botsAccountGroupUuid =
+ groupOperations.newGroup().name("bots").addMember(bot.id()).create();
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allowCapability("checks-administrateCheckers").group(botsAccountGroupUuid))
+ .update();
+
+ // Create two required checkers.
+ checkerUuid1 = checkerOperations.newChecker().repository(project).required().create();
+ checkerUuid2 = checkerOperations.newChecker().repository(project).required().create();
+
+ // Create a change.
+ owner = admin;
+ patchSetId = createChange().getPatchSetId();
+
+ // Add a reviewer.
+ reviewer = accountCreator.create("reviewer", "reviewer@test.com", "Reviewer");
+ gApi.changes().id(patchSetId.changeId().get()).addReviewer(reviewer.username());
+
+ // Star the change from some user.
+ starrer = accountCreator.create("starred", "starrer@test.com", "Starrer");
+ requestScopeOperations.setApiUser(starrer.id());
+ gApi.accounts().self().starChange(patchSetId.changeId().toString());
+
+ // Watch all comments of change from some user.
+ watcher = accountCreator.create("watcher", "watcher@test.com", "Watcher");
+ requestScopeOperations.setApiUser(watcher.id());
+ ProjectWatchInfo projectWatchInfo = new ProjectWatchInfo();
+ projectWatchInfo.project = project.get();
+ projectWatchInfo.filter = "*";
+ projectWatchInfo.notifyAllComments = true;
+ gApi.accounts().self().setWatchedProjects(ImmutableList.of(projectWatchInfo));
+
+ // Watch only change creations from some user --> user doesn't get notified by checks plugin.
+ TestAccount changeCreationWatcher =
+ accountCreator.create(
+ "changeCreationWatcher", "changeCreationWatcher@test.com", "Change Creation Watcher");
+ requestScopeOperations.setApiUser(changeCreationWatcher.id());
+ projectWatchInfo = new ProjectWatchInfo();
+ projectWatchInfo.project = project.get();
+ projectWatchInfo.filter = "*";
+ projectWatchInfo.notifyNewChanges = true;
+ gApi.accounts().self().setWatchedProjects(ImmutableList.of(projectWatchInfo));
+
+ // Add a reviewer that ignores the change --> user doesn't get notified by checks plugin.
+ TestAccount ignorer = accountCreator.create("ignorer", "ignorer@test.com", "Ignorer");
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(patchSetId.changeId().get()).addReviewer(ignorer.username());
+ requestScopeOperations.setApiUser(ignorer.id());
+ gApi.changes().id(patchSetId.changeId().get()).ignore(true);
+
+ // Reset request scope to admin.
+ requestScopeOperations.setApiUser(admin.id());
+ }
+
+ @Test
+ public void combinedCheckUpdatedEmailAfterCheckCreation() throws Exception {
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.IN_PROGRESS);
+
+ sender.clear();
+
+ // Post a new check that changes the combined check state to FAILED.
+ requestScopeOperations.setApiUser(bot.id());
+ CheckInput input = new CheckInput();
+ input.checkerUuid = checkerUuid1.get();
+ input.state = CheckState.FAILED;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ // Except one email because the combined check state was updated.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+
+ Message message = messages.get(0);
+ assertThat(message.from().getName()).isEqualTo(bot.fullName() + " (Code Review)");
+ assertThat(message.body())
+ .contains("The combined check state has been updated to " + CombinedCheckState.FAILED);
+ assertThat(message.rcpt())
+ .containsExactly(
+ owner.getEmailAddress(),
+ reviewer.getEmailAddress(),
+ starrer.getEmailAddress(),
+ watcher.getEmailAddress());
+ }
+
+ @Test
+ public void noCombinedCheckUpdatedEmailOnCheckCreationIfCombinedCheckStateIsNotChanged()
+ throws Exception {
+ // Create a check that sets the combined check state to FAILED.
+ CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid1);
+ checkOperations.newCheck(checkKey).state(CheckState.FAILED).upsert();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ sender.clear();
+
+ // Post a new check that doesn't change the combined check state..
+ requestScopeOperations.setApiUser(bot.id());
+ CheckInput input = new CheckInput();
+ input.checkerUuid = checkerUuid2.get();
+ input.state = CheckState.SCHEDULED;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ // Except that no email was sent because the combined check state was not updated.
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void combinedCheckUpdatedEmailAfterCheckUpdate() throws Exception {
+ // Create a check that sets the combined check state to FAILED.
+ CheckKey checkKey = CheckKey.create(project, patchSetId, checkerUuid1);
+ checkOperations.newCheck(checkKey).state(CheckState.FAILED).upsert();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ sender.clear();
+
+ // Update the new check so that the combined check state is changed to IN_PROGRESS.
+ requestScopeOperations.setApiUser(bot.id());
+ CheckInput input = new CheckInput();
+ input.checkerUuid = checkerUuid1.get();
+ input.state = CheckState.RUNNING;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.IN_PROGRESS);
+
+ // Except one email because the combined check state was updated.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+
+ Message message = messages.get(0);
+ assertThat(message.from().getName()).isEqualTo(bot.fullName() + " (Code Review)");
+ assertThat(message.body())
+ .contains("The combined check state has been updated to " + CombinedCheckState.IN_PROGRESS);
+ assertThat(message.rcpt())
+ .containsExactly(
+ owner.getEmailAddress(),
+ reviewer.getEmailAddress(),
+ starrer.getEmailAddress(),
+ watcher.getEmailAddress());
+ }
+
+ @Test
+ public void noCombinedCheckUpdatedEmailOnCheckUpdateIfCombinedCheckStateIsNotChanged()
+ throws Exception {
+ // Create 2 checks that set the combined check state to FAILED.
+ CheckKey checkKey1 = CheckKey.create(project, patchSetId, checkerUuid1);
+ checkOperations.newCheck(checkKey1).state(CheckState.FAILED).upsert();
+ CheckKey checkKey2 = CheckKey.create(project, patchSetId, checkerUuid2);
+ checkOperations.newCheck(checkKey2).state(CheckState.FAILED).upsert();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ sender.clear();
+
+ // Update one of the checks in a way so that doesn't change the combined check state..
+ requestScopeOperations.setApiUser(bot.id());
+ CheckInput input = new CheckInput();
+ input.checkerUuid = checkerUuid2.get();
+ input.state = CheckState.SCHEDULED;
+ checksApiFactory.revision(patchSetId).create(input).get();
+ assertThat(getCombinedCheckState()).isEqualTo(CombinedCheckState.FAILED);
+
+ // Except that no email was sent because the combined check state was not updated.
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ private CombinedCheckState getCombinedCheckState() throws RestApiException {
+ ChangeInfo changeInfo =
+ gApi.changes()
+ .id(patchSetId.changeId().get())
+ .get(ImmutableListMultimap.of("checks--combined", "true"));
+ ImmutableList<PluginDefinedInfo> infos =
+ changeInfo.plugins.stream().filter(i -> i.name.equals("checks")).collect(toImmutableList());
+ assertThat(infos).hasSize(1);
+ assertThat(infos.get(0)).isInstanceOf(ChangeCheckInfo.class);
+ return ((ChangeCheckInfo) infos.get(0)).combinedState;
+ }
+}
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
index 6458099..1948adb 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckOperationsImplTest.java
@@ -41,7 +41,6 @@
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Optional;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
@@ -91,22 +90,27 @@
CheckKey.create(
Project.nameKey("non-existing"), createChange().getPatchSetId(), checkerUuid);
- IllegalStateException thrown =
- assertThrows(
- IllegalStateException.class, () -> checkOperations.newCheck(checkKey).upsert());
- assertThat(thrown.getCause(), instanceOf(RepositoryNotFoundException.class));
+ assertThrows(IllegalStateException.class, () -> checkOperations.newCheck(checkKey).upsert());
+ }
+
+ @Test
+ public void checkCannotBeCreatedForNonExistingChange() throws Exception {
+ CheckerUuid checkerUuid = checkerOperations.newChecker().create();
+ CheckKey checkKey = CheckKey.create(project, PatchSet.id(Change.id(1), 1), checkerUuid);
+
+ assertThrows(IllegalStateException.class, () -> checkOperations.newCheck(checkKey).upsert());
}
@Test
public void checkCannotBeCreatedForNonExistingPatchSet() throws Exception {
+ Change.Id changeId = createChange().getChange().getId();
CheckerUuid checkerUuid = checkerOperations.newChecker().create();
- CheckKey checkKey = CheckKey.create(project, PatchSet.id(Change.id(1), 1), checkerUuid);
+ CheckKey checkKey = CheckKey.create(project, PatchSet.id(changeId, 99), checkerUuid);
IllegalStateException thrown =
assertThrows(
IllegalStateException.class, () -> checkOperations.newCheck(checkKey).upsert());
- assertThat(thrown.getCause(), instanceOf(IOException.class));
- assertThat(thrown).hasMessageThat().contains("patchset 1,1 not found");
+ assertThat(thrown).hasMessageThat().contains("patch set not found: " + changeId.get() + ",99");
}
@Test
diff --git a/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy
new file mode 100644
index 0000000..83d67b1
--- /dev/null
+++ b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdated.soy
@@ -0,0 +1,41 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .CombinedCheckStateUpdated template will determine the contents of the email related to a
+ * change for which the combined check state was updated.
+ */
+{template .CombinedCheckStateUpdated kind="text"}
+ {@param change: ?}
+ {@param combinedCheckState: ?}
+ {@param coverLetter: ?}
+ {@param email: ?}
+ {@param patchSet: ?}
+ The combined check state has been updated to {$combinedCheckState} for patch set
+ {$patchSet.patchSetId} of this change.
+ {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+ {\n}
+ Change subject: {$change.subject}{\n}
+ ......................................................................{\n}
+ {if $coverLetter}
+ {\n}
+ {\n}
+ {$coverLetter}
+ {\n}
+ {/if}
+{/template}
diff --git a/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy
new file mode 100644
index 0000000..8e7e5ff
--- /dev/null
+++ b/resources/com/google/gerrit/plugins/checks/email/CombinedCheckStateUpdatedHtml.soy
@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .CombinedCheckStateUpdatedHtml template will determine the contents of the email related to
+ * a change for which the combined check state was updated.
+ */
+{template .CombinedCheckStateUpdatedHtml}
+ {@param combinedCheckState: ?}
+ {@param coverLetter: ?}
+ {@param email: ?}
+ {@param patchSet: ?}
+ <p>
+ The combined check state has been updated to <strong>{$combinedCheckState}</strong> for patch
+ set {$patchSet.patchSetId} of this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ {if $coverLetter}
+ <div style="white-space:pre-wrap">{$coverLetter}</div>
+ {/if}
+{/template}