| // 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 com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestProjectInput; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| 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.client.SubmitType; |
| 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.extensions.registration.DynamicSet; |
| import com.google.gerrit.extensions.registration.RegistrationHandle; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| 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.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| public class ActionsIT extends AbstractDaemonTest { |
| @ConfigSuite.Config |
| public static Config submitWholeTopicEnabled() { |
| return submitWholeTopicEnabledConfig(); |
| } |
| |
| @Inject private DynamicSet<ActionVisitor> actionVisitors; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private RevisionJson.Factory revisionJsonFactory; |
| |
| private RegistrationHandle visitorHandle; |
| |
| @Before |
| public void setUp() { |
| visitorHandle = null; |
| } |
| |
| @After |
| public void tearDown() { |
| if (visitorHandle != null) { |
| visitorHandle.remove(); |
| } |
| } |
| |
| protected Map<String, ActionInfo> getActions(String id) throws Exception { |
| return gApi.changes().id(id).revision(1).actions(); |
| } |
| |
| protected String getETag(String id) throws Exception { |
| return gApi.changes().id(id).current().etag(); |
| } |
| |
| @Test |
| public void revisionActionsOneChangePerTopicUnapproved() throws Exception { |
| String changeId = createChangeWithTopic().getChangeId(); |
| Map<String, ActionInfo> actions = getActions(changeId); |
| assertThat(actions).hasSize(3); |
| assertThat(actions).containsKey("cherrypick"); |
| assertThat(actions).containsKey("rebase"); |
| assertThat(actions).containsKey("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 { |
| String changeId = createChangeWithTopic().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).matches("Change " + legacyId2 + " is not ready: needs Code-Review"); |
| } else { |
| noSubmitWholeTopicAssertions(actions, 1); |
| |
| assertThat(getActions(changeId2).get("submit")).isNull(); |
| approve(changeId2); |
| noSubmitWholeTopicAssertions(getActions(changeId2), 2); |
| } |
| } |
| |
| @Test |
| public void revisionActionsETag() throws Exception { |
| String parent = createChange().getChangeId(); |
| String change = createChangeWithTopic().getChangeId(); |
| approve(change); |
| String etag1 = getETag(change); |
| |
| approve(parent); |
| String etag2 = getETag(change); |
| |
| String changeWithSameTopic = createChangeWithTopic().getChangeId(); |
| String etag3 = getETag(change); |
| |
| approve(changeWithSameTopic); |
| String etag4 = getETag(change); |
| |
| if (isSubmitWholeTopicEnabled()) { |
| assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates(); |
| } else { |
| assertThat(etag2).isNotEqualTo(etag1); |
| assertThat(etag3).isEqualTo(etag2); |
| assertThat(etag4).isEqualTo(etag2); |
| } |
| } |
| |
| @Test |
| public void revisionActionsAnonymousETag() throws Exception { |
| String parent = createChange().getChangeId(); |
| String change = createChangeWithTopic().getChangeId(); |
| approve(change); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag1 = getETag(change); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| approve(parent); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag2 = getETag(change); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| String changeWithSameTopic = createChangeWithTopic().getChangeId(); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag3 = getETag(change); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| approve(changeWithSameTopic); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag4 = getETag(change); |
| |
| if (isSubmitWholeTopicEnabled()) { |
| assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates(); |
| } else { |
| assertThat(etag2).isNotEqualTo(etag1); |
| assertThat(etag3).isEqualTo(etag2); |
| assertThat(etag4).isEqualTo(etag2); |
| } |
| } |
| |
| @Test |
| @TestProjectInput(submitType = SubmitType.CHERRY_PICK) |
| public void revisionActionsAnonymousETagCherryPickStrategy() throws Exception { |
| String parent = createChange().getChangeId(); |
| String change = createChange().getChangeId(); |
| approve(change); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag1 = getETag(change); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| approve(parent); |
| |
| requestScopeOperations.setApiUserAnonymous(); |
| String etag2 = getETag(change); |
| assertThat(etag2).isEqualTo(etag1); |
| } |
| |
| @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"); |
| assertThat(info.enabled).isNull(); |
| assertThat(info.label).isEqualTo("Submit whole topic"); |
| assertThat(info.method).isEqualTo("POST"); |
| assertThat(info.title).isEqualTo("Problems with change(s): " + changeNum2); |
| } 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"); |
| |
| Visitor v = new Visitor(); |
| visitorHandle = actionVisitors.add("gerrit", v); |
| |
| 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 = new 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"); |
| |
| Visitor v = new Visitor(); |
| visitorHandle = actionVisitors.add("gerrit", v); |
| |
| // 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); |
| revisionJsonFactory.create(opts).getRevisionInfo(cd, cd.patchSet(new PatchSet.Id(changeId, 1))); |
| } |
| |
| 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"); |
| |
| Visitor v = new Visitor(); |
| visitorHandle = actionVisitors.add("gerrit", v); |
| |
| // 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) { |
| assertThat(actions).hasSize(4); |
| assertThat(actions).containsKey("cherrypick"); |
| assertThat(actions).containsKey("submit"); |
| assertThat(actions).containsKey("description"); |
| assertThat(actions).containsKey("rebase"); |
| } |
| |
| private PushOneCommit.Result createChangeWithTopic() throws Exception { |
| return createChangeWithTopic(testRepo, "topic", "message", "a.txt", "content\n"); |
| } |
| } |