| // Copyright (C) 2015 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.acceptance.rest.change; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS; |
| import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; |
| import static com.google.gerrit.truth.MapSubject.assertThatMap; |
| |
| import com.google.common.collect.Iterables; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.ExtensionRegistry; |
| import com.google.gerrit.acceptance.ExtensionRegistry.Registration; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.extensions.api.changes.ActionVisitor; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.common.ActionInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.server.change.MergeabilityComputationBehavior; |
| import com.google.gerrit.server.change.RevisionJson; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.testing.ConfigSuite; |
| import com.google.inject.Inject; |
| import java.util.EnumSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import org.eclipse.jgit.lib.Config; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| public class ActionsIT extends AbstractDaemonTest { |
| @ConfigSuite.Config |
| public static Config submitWholeTopicEnabled() { |
| return submitWholeTopicEnabledConfig(); |
| } |
| |
| @ConfigSuite.Config |
| public static Config mergeabilityCheckEnabled() { |
| Config cfg = new Config(); |
| cfg.setEnum( |
| "change", |
| null, |
| "mergeabilityComputationBehavior", |
| MergeabilityComputationBehavior.API_REF_UPDATED_AND_CHANGE_REINDEX); |
| return cfg; |
| } |
| |
| @Inject private RevisionJson.Factory revisionJsonFactory; |
| @Inject private ExtensionRegistry extensionRegistry; |
| |
| private MergeabilityComputationBehavior mcb; |
| |
| protected Map<String, ActionInfo> getActions(String id) throws Exception { |
| return gApi.changes().id(id).revision(1).actions(); |
| } |
| |
| protected Map<String, ActionInfo> getChangeActions(String id) throws Exception { |
| return gApi.changes().id(id).get().actions; |
| } |
| |
| @Before |
| public void setUp() { |
| mcb = MergeabilityComputationBehavior.fromConfig(cfg); |
| } |
| |
| @Test |
| public void changeActionOneMergedChangeHasOnlyNormalRevert() throws Exception { |
| String changeId = createChangeWithTopic().getChangeId(); |
| gApi.changes().id(changeId).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeId).current().submit(); |
| Map<String, ActionInfo> actions = getChangeActions(changeId); |
| assertThat(actions).containsKey("revert"); |
| assertThat(actions).doesNotContainKey("revert_submission"); |
| } |
| |
| @Test |
| public void changeActionTwoMergedChangesHaveReverts() throws Exception { |
| String changeId1 = createChangeWithTopic().getChangeId(); |
| String changeId2 = createChangeWithTopic().getChangeId(); |
| gApi.changes().id(changeId1).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeId2).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeId2).current().submit(); |
| Map<String, ActionInfo> actions1 = getChangeActions(changeId1); |
| assertThatMap(actions1).keys().containsAtLeast("revert", "revert_submission"); |
| Map<String, ActionInfo> actions2 = getChangeActions(changeId2); |
| assertThatMap(actions2).keys().containsAtLeast("revert", "revert_submission"); |
| } |
| |
| @Test |
| public void revisionActionsOneChangePerTopicUnapproved() throws Exception { |
| String changeId = createChangeWithTopic().getChangeId(); |
| Map<String, ActionInfo> actions = getActions(changeId); |
| assertThatMap(actions).keys().containsExactly("cherrypick", "rebase", "description"); |
| } |
| |
| @Test |
| public void revisionActionsOneChangePerTopic() throws Exception { |
| String changeId = createChangeWithTopic().getChangeId(); |
| approve(changeId); |
| Map<String, ActionInfo> actions = getActions(changeId); |
| commonActionsAssertions(actions); |
| // We want to treat a single change in a topic not as a whole topic, |
| // so regardless of how submitWholeTopic is configured: |
| noSubmitWholeTopicAssertions(actions, 1); |
| } |
| |
| @Test |
| public void revisionActionsTwoChangesInTopic() throws Exception { |
| PushOneCommit.Result change1 = createChangeWithTopic(); |
| String changeId = change1.getChangeId(); |
| approve(changeId); |
| PushOneCommit.Result change2 = createChangeWithTopic(); |
| int legacyId2 = change2.getChange().getId().get(); |
| String changeId2 = change2.getChangeId(); |
| Map<String, ActionInfo> actions = getActions(changeId); |
| commonActionsAssertions(actions); |
| if (isSubmitWholeTopicEnabled()) { |
| ActionInfo info = actions.get("submit"); |
| assertThat(info.enabled).isNull(); |
| assertThat(info.label).isEqualTo("Submit whole topic"); |
| assertThat(info.method).isEqualTo("POST"); |
| assertThat(info.title) |
| .startsWith( |
| "Change " |
| + change1.getChange().getId() |
| + " must be submitted with change " |
| + legacyId2 |
| + " but " |
| + legacyId2 |
| + " is not ready: submit requirement 'Code-Review' is unsatisfied."); |
| } else { |
| noSubmitWholeTopicAssertions(actions, 1); |
| |
| assertThat(getActions(changeId2).get("submit")).isNull(); |
| approve(changeId2); |
| noSubmitWholeTopicAssertions(getActions(changeId2), 2); |
| } |
| } |
| |
| @Test |
| public void revisionActionsTwoChangesInTopic_conflicting() throws Exception { |
| String changeId = createChangeWithTopic().getChangeId(); |
| approve(changeId); |
| |
| // create another change with the same topic |
| String changeId2 = |
| createChangeWithTopic(testRepo, "topic", "touching b", "b.txt", "real content") |
| .getChangeId(); |
| int changeNum2 = gApi.changes().id(changeId2).info()._number; |
| approve(changeId2); |
| |
| // collide with the other change in the same topic |
| testRepo.reset("HEAD~2"); |
| String collidingChange = |
| createChangeWithTopic( |
| testRepo, "off_topic", "rewriting file b", "b.txt", "garbage\ngarbage\ngarbage") |
| .getChangeId(); |
| gApi.changes().id(collidingChange).current().review(ReviewInput.approve()); |
| gApi.changes().id(collidingChange).current().submit(); |
| |
| Map<String, ActionInfo> actions = getActions(changeId); |
| commonActionsAssertions(actions); |
| if (isSubmitWholeTopicEnabled()) { |
| ActionInfo info = actions.get("submit"); |
| if (mcb != MergeabilityComputationBehavior.NEVER) { |
| assertThat(info.enabled).isNull(); |
| assertThat(info.title).isEqualTo("Problems with change(s): " + changeNum2); |
| } |
| assertThat(info.label).isEqualTo("Submit whole topic"); |
| assertThat(info.method).isEqualTo("POST"); |
| } else { |
| noSubmitWholeTopicAssertions(actions, 1); |
| } |
| } |
| |
| @Test |
| public void revisionActionsTwoChangesInTopicWithAncestorReady() throws Exception { |
| String changeId = createChange().getChangeId(); |
| approve(changeId); |
| approve(changeId); |
| String changeId1 = createChangeWithTopic().getChangeId(); |
| approve(changeId1); |
| // create another change with the same topic |
| String changeId2 = createChangeWithTopic().getChangeId(); |
| approve(changeId2); |
| Map<String, ActionInfo> actions = getActions(changeId1); |
| commonActionsAssertions(actions); |
| if (isSubmitWholeTopicEnabled()) { |
| ActionInfo info = actions.get("submit"); |
| assertThat(info.enabled).isTrue(); |
| assertThat(info.label).isEqualTo("Submit whole topic"); |
| assertThat(info.method).isEqualTo("POST"); |
| assertThat(info.title) |
| .isEqualTo( |
| "Submit all 2 changes of the same " |
| + "topic (3 changes including ancestors " |
| + "and other changes related by topic)"); |
| } else { |
| noSubmitWholeTopicAssertions(actions, 2); |
| } |
| } |
| |
| @Test |
| public void revisionActionsReadyWithAncestors() throws Exception { |
| String changeId = createChange().getChangeId(); |
| approve(changeId); |
| approve(changeId); |
| String changeId1 = createChange().getChangeId(); |
| approve(changeId1); |
| String changeId2 = createChangeWithTopic().getChangeId(); |
| approve(changeId2); |
| Map<String, ActionInfo> actions = getActions(changeId2); |
| commonActionsAssertions(actions); |
| // The topic contains only one change, so standard text applies |
| noSubmitWholeTopicAssertions(actions, 3); |
| } |
| |
| private void noSubmitWholeTopicAssertions(Map<String, ActionInfo> actions, int nrChanges) { |
| ActionInfo info = actions.get("submit"); |
| assertThat(info.enabled).isTrue(); |
| if (nrChanges == 1) { |
| assertThat(info.label).isEqualTo("Submit"); |
| } else { |
| assertThat(info.label).isEqualTo("Submit including parents"); |
| } |
| assertThat(info.method).isEqualTo("POST"); |
| if (nrChanges == 1) { |
| assertThat(info.title).isEqualTo("Submit patch set 1 into master"); |
| } else { |
| assertThat(info.title) |
| .isEqualTo( |
| String.format( |
| "Submit patch set 1 and ancestors (%d changes altogether) into master", |
| nrChanges)); |
| } |
| } |
| |
| @Test |
| public void changeActionVisitor() throws Exception { |
| String id = createChange().getChangeId(); |
| ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS); |
| |
| class Visitor implements ActionVisitor { |
| @Override |
| public boolean visit(String name, ActionInfo actionInfo, ChangeInfo changeInfo) { |
| assertThat(changeInfo).isNotNull(); |
| assertThat(changeInfo._number).isEqualTo(origChange._number); |
| if (name.equals("followup")) { |
| return false; |
| } |
| if (name.equals("abandon")) { |
| actionInfo.label = "Abandon All Hope"; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean visit( |
| String name, ActionInfo actionInfo, ChangeInfo changeInfo, RevisionInfo revisionInfo) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| |
| Map<String, ActionInfo> origActions = origChange.actions; |
| assertThat(origActions.keySet()).containsAtLeast("followup", "abandon"); |
| assertThat(origActions.get("abandon").label).isEqualTo("Abandon"); |
| |
| try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) { |
| Map<String, ActionInfo> newActions = |
| gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)).actions; |
| |
| Set<String> expectedNames = new TreeSet<>(origActions.keySet()); |
| expectedNames.remove("followup"); |
| assertThat(newActions.keySet()).isEqualTo(expectedNames); |
| |
| ActionInfo abandon = newActions.get("abandon"); |
| assertThat(abandon).isNotNull(); |
| assertThat(abandon.label).isEqualTo("Abandon All Hope"); |
| } |
| } |
| |
| @Test |
| public void currentRevisionActionVisitor() throws Exception { |
| String id = createChange().getChangeId(); |
| amendChange(id); |
| ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS); |
| Change.Id changeId = Change.id(origChange._number); |
| |
| class Visitor implements ActionVisitor { |
| @Override |
| public boolean visit(String name, ActionInfo actionInfo, ChangeInfo changeInfo) { |
| return true; // Do nothing; implicitly called for CURRENT_ACTIONS. |
| } |
| |
| @Override |
| public boolean visit( |
| String name, ActionInfo actionInfo, ChangeInfo changeInfo, RevisionInfo revisionInfo) { |
| assertThat(changeInfo).isNotNull(); |
| assertThat(changeInfo._number).isEqualTo(origChange._number); |
| assertThat(revisionInfo).isNotNull(); |
| assertThat(revisionInfo._number).isEqualTo(2); |
| if (name.equals("cherrypick")) { |
| return false; |
| } |
| if (name.equals("rebase")) { |
| actionInfo.label = "All Your Base"; |
| } |
| return true; |
| } |
| } |
| |
| Map<String, ActionInfo> origActions = gApi.changes().id(id).current().actions(); |
| assertThat(origActions.keySet()).containsAtLeast("cherrypick", "rebase"); |
| assertThat(origActions.get("rebase").label).isEqualTo("Rebase"); |
| |
| try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) { |
| // Test different codepaths within ActionJson... |
| // ...via revision API. |
| visitedCurrentRevisionActionsAssertions( |
| origActions, gApi.changes().id(id).current().actions()); |
| |
| // ...via change API with option. |
| EnumSet<ListChangesOption> opts = EnumSet.of(CURRENT_ACTIONS, CURRENT_REVISION); |
| ChangeInfo changeInfo = gApi.changes().id(id).get(opts); |
| RevisionInfo revisionInfo = Iterables.getOnlyElement(changeInfo.revisions.values()); |
| visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions); |
| |
| // ...via ChangeJson directly. |
| ChangeData cd = changeDataFactory.create(project, changeId); |
| revisionInfo = |
| revisionJsonFactory |
| .create(opts) |
| .getRevisionInfo(cd, cd.patchSet(PatchSet.id(changeId, revisionInfo._number))); |
| visitedCurrentRevisionActionsAssertions(origActions, revisionInfo.actions); |
| } |
| } |
| |
| private void visitedCurrentRevisionActionsAssertions( |
| Map<String, ActionInfo> origActions, Map<String, ActionInfo> newActions) { |
| assertThat(newActions).isNotNull(); |
| Set<String> expectedNames = new TreeSet<>(origActions.keySet()); |
| expectedNames.remove("cherrypick"); |
| assertThat(newActions.keySet()).isEqualTo(expectedNames); |
| |
| ActionInfo rebase = newActions.get("rebase"); |
| assertThat(rebase).isNotNull(); |
| assertThat(rebase.label).isEqualTo("All Your Base"); |
| } |
| |
| @Test |
| public void oldRevisionActionVisitor() throws Exception { |
| String id = createChange().getChangeId(); |
| amendChange(id); |
| ChangeInfo origChange = gApi.changes().id(id).get(CHANGE_ACTIONS); |
| |
| class Visitor implements ActionVisitor { |
| @Override |
| public boolean visit(String name, ActionInfo actionInfo, ChangeInfo changeInfo) { |
| return true; // Do nothing; implicitly called for CURRENT_ACTIONS. |
| } |
| |
| @Override |
| public boolean visit( |
| String name, ActionInfo actionInfo, ChangeInfo changeInfo, RevisionInfo revisionInfo) { |
| assertThat(changeInfo).isNotNull(); |
| assertThat(changeInfo._number).isEqualTo(origChange._number); |
| assertThat(revisionInfo).isNotNull(); |
| assertThat(revisionInfo._number).isEqualTo(1); |
| if (name.equals("description")) { |
| actionInfo.label = "Describify"; |
| } |
| return true; |
| } |
| } |
| |
| Map<String, ActionInfo> origActions = gApi.changes().id(id).revision(1).actions(); |
| assertThat(origActions.keySet()).containsExactly("description"); |
| assertThat(origActions.get("description").label).isEqualTo("Edit Description"); |
| |
| try (Registration registration = extensionRegistry.newRegistration().add(new Visitor())) { |
| // Unlike for the current revision, actions for old revisions are only available via the |
| // revision API. |
| Map<String, ActionInfo> newActions = gApi.changes().id(id).revision(1).actions(); |
| assertThat(newActions).isNotNull(); |
| assertThat(newActions.keySet()).isEqualTo(origActions.keySet()); |
| |
| ActionInfo description = newActions.get("description"); |
| assertThat(description).isNotNull(); |
| assertThat(description.label).isEqualTo("Describify"); |
| } |
| } |
| |
| private void commonActionsAssertions(Map<String, ActionInfo> actions) { |
| assertThatMap(actions).keys().containsExactly("cherrypick", "submit", "description", "rebase"); |
| } |
| |
| private PushOneCommit.Result createChangeWithTopic() throws Exception { |
| return createChangeWithTopic(testRepo, "topic", "message", "a.txt", "content\n"); |
| } |
| } |