|  | // Copyright (C) 2016 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.api.revision; | 
|  |  | 
|  | import static com.google.common.truth.Truth.assertThat; | 
|  | import static com.google.common.truth.TruthJUnit.assume; | 
|  | import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; | 
|  | import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat; | 
|  | import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList; | 
|  | import static java.util.stream.Collectors.toList; | 
|  |  | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.collect.Iterables; | 
|  | import com.google.gerrit.acceptance.AbstractDaemonTest; | 
|  | import com.google.gerrit.acceptance.AcceptanceTestRequestScope; | 
|  | import com.google.gerrit.acceptance.GerritConfig; | 
|  | import com.google.gerrit.acceptance.PushOneCommit; | 
|  | import com.google.gerrit.extensions.api.changes.ReviewInput; | 
|  | import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; | 
|  | import com.google.gerrit.extensions.client.Comment; | 
|  | import com.google.gerrit.extensions.common.ChangeInfo; | 
|  | import com.google.gerrit.extensions.common.EditInfo; | 
|  | import com.google.gerrit.extensions.common.FixReplacementInfo; | 
|  | import com.google.gerrit.extensions.common.FixSuggestionInfo; | 
|  | import com.google.gerrit.extensions.common.RobotCommentInfo; | 
|  | import com.google.gerrit.extensions.restapi.BadRequestException; | 
|  | import com.google.gerrit.extensions.restapi.BinaryResult; | 
|  | import com.google.gerrit.extensions.restapi.MethodNotAllowedException; | 
|  | import com.google.gerrit.extensions.restapi.ResourceConflictException; | 
|  | import com.google.gerrit.extensions.restapi.ResourceNotFoundException; | 
|  | import com.google.gerrit.extensions.restapi.RestApiException; | 
|  | import com.google.gerrit.extensions.restapi.testing.BinaryResultSubject; | 
|  | import java.util.Arrays; | 
|  | import java.util.Collections; | 
|  | import java.util.HashMap; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Objects; | 
|  | import java.util.Optional; | 
|  | import org.junit.Before; | 
|  | import org.junit.Test; | 
|  |  | 
|  | public class RobotCommentsIT extends AbstractDaemonTest { | 
|  | private static final String FILE_NAME = "file_to_fix.txt"; | 
|  | private static final String FILE_NAME2 = "another_file_to_fix.txt"; | 
|  | private static final String FILE_CONTENT = | 
|  | "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line" | 
|  | + "\nSeventh line\nEighth line\nNinth line\nTenth line\n"; | 
|  | private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n"; | 
|  |  | 
|  | private String changeId; | 
|  | private FixReplacementInfo fixReplacementInfo; | 
|  | private FixSuggestionInfo fixSuggestionInfo; | 
|  | private RobotCommentInput withFixRobotCommentInput; | 
|  |  | 
|  | @Before | 
|  | public void setUp() throws Exception { | 
|  | PushOneCommit push = | 
|  | pushFactory.create( | 
|  | db, | 
|  | admin.getIdent(), | 
|  | testRepo, | 
|  | "Provide files which can be used for fixes", | 
|  | ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2)); | 
|  | PushOneCommit.Result changeResult = push.to("refs/for/master"); | 
|  | changeId = changeResult.getChangeId(); | 
|  |  | 
|  | fixReplacementInfo = createFixReplacementInfo(); | 
|  | fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfo); | 
|  | withFixRobotCommentInput = createRobotCommentInput(fixSuggestionInfo); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void retrievingRobotCommentsBeforeAddingAnyDoesNotRaiseAnException() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | Map<String, List<RobotCommentInfo>> robotComments = | 
|  | gApi.changes().id(changeId).current().robotComments(); | 
|  |  | 
|  | assertThat(robotComments).isNotNull(); | 
|  | assertThat(robotComments).isEmpty(); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void addedRobotCommentsCanBeRetrieved() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | RobotCommentInput in = createRobotCommentInput(); | 
|  | addRobotComment(changeId, in); | 
|  |  | 
|  | Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).current().robotComments(); | 
|  |  | 
|  | assertThat(out).hasSize(1); | 
|  | RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path)); | 
|  | assertRobotComment(comment, in, false); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void addedRobotCommentsCanBeRetrievedByChange() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | RobotCommentInput in = createRobotCommentInput(); | 
|  | addRobotComment(changeId, in); | 
|  |  | 
|  | pushFactory.create(db, admin.getIdent(), testRepo, changeId).to("refs/for/master"); | 
|  |  | 
|  | RobotCommentInput in2 = createRobotCommentInput(); | 
|  | addRobotComment(changeId, in2); | 
|  |  | 
|  | Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).robotComments(); | 
|  |  | 
|  | assertThat(out).hasSize(1); | 
|  | assertThat(out.get(in.path)).hasSize(2); | 
|  |  | 
|  | RobotCommentInfo comment1 = out.get(in.path).get(0); | 
|  | assertRobotComment(comment1, in, false); | 
|  | RobotCommentInfo comment2 = out.get(in.path).get(1); | 
|  | assertRobotComment(comment2, in2, false); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void robotCommentsCanBeRetrievedAsList() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | RobotCommentInput robotCommentInput = createRobotCommentInput(); | 
|  | addRobotComment(changeId, robotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = | 
|  | gApi.changes().id(changeId).current().robotCommentsAsList(); | 
|  |  | 
|  | assertThat(robotCommentInfos).hasSize(1); | 
|  | RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos); | 
|  | assertRobotComment(robotCommentInfo, robotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void specificRobotCommentCanBeRetrieved() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | RobotCommentInput robotCommentInput = createRobotCommentInput(); | 
|  | addRobotComment(changeId, robotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos); | 
|  |  | 
|  | RobotCommentInfo specificRobotCommentInfo = | 
|  | gApi.changes().id(changeId).current().robotComment(robotCommentInfo.id).get(); | 
|  | assertRobotComment(specificRobotCommentInfo, robotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void robotCommentWithoutOptionalFieldsCanBeAdded() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); | 
|  | addRobotComment(changeId, in); | 
|  |  | 
|  | Map<String, List<RobotCommentInfo>> out = gApi.changes().id(changeId).current().robotComments(); | 
|  | assertThat(out).hasSize(1); | 
|  | RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path)); | 
|  | assertRobotComment(comment, in, false); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void hugeRobotCommentIsRejected() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | int defaultSizeLimit = 1024 * 1024; | 
|  | int sizeOfRest = 451; | 
|  | fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest + 1); | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage("limit"); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void reasonablyLargeRobotCommentIsAccepted() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | int defaultSizeLimit = 1024 * 1024; | 
|  | int sizeOfRest = 451; | 
|  | fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThat(robotCommentInfos).hasSize(1); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | @GerritConfig(name = "change.robotCommentSizeLimit", value = "10k") | 
|  | public void maximumAllowedSizeOfRobotCommentCanBeAdjusted() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | int sizeLimit = 10 * 1024; | 
|  | fixReplacementInfo.replacement = getStringFor(sizeLimit); | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage("limit"); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | @GerritConfig(name = "change.robotCommentSizeLimit", value = "0") | 
|  | public void zeroForMaximumAllowedSizeOfRobotCommentRemovesRestriction() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | int defaultSizeLimit = 1024 * 1024; | 
|  | fixReplacementInfo.replacement = getStringFor(defaultSizeLimit); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThat(robotCommentInfos).hasSize(1); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | @GerritConfig(name = "change.robotCommentSizeLimit", value = "-1") | 
|  | public void negativeValueForMaximumAllowedSizeOfRobotCommentRemovesRestriction() | 
|  | throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | int defaultSizeLimit = 1024 * 1024; | 
|  | fixReplacementInfo.replacement = getStringFor(defaultSizeLimit); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThat(robotCommentInfos).hasSize(1); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void addedFixSuggestionCanBeRetrieved() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().isNotNull(); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixIdIsGeneratedForFixSuggestion() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().fixId().isNotEmpty(); | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .fixId() | 
|  | .isNotEqualTo(fixSuggestionInfo.fixId); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .description() | 
|  | .isEqualTo(fixSuggestionInfo.description); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void descriptionOfFixSuggestionIsMandatory() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixSuggestionInfo.description = null; | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage( | 
|  | String.format( | 
|  | "A description is required for the suggested fix of the robot comment on %s", | 
|  | withFixRobotCommentInput.path)); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void addedFixReplacementCanBeRetrieved() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .onlyReplacement() | 
|  | .isNotNull(); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixReplacementsAreMandatory() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixSuggestionInfo.replacements = Collections.emptyList(); | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage( | 
|  | String.format( | 
|  | "At least one replacement is required" | 
|  | + " for the suggested fix of the robot comment on %s", | 
|  | withFixRobotCommentInput.path)); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void pathOfFixReplacementIsAcceptedAsIs() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .onlyReplacement() | 
|  | .path() | 
|  | .isEqualTo(fixReplacementInfo.path); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void pathOfFixReplacementIsMandatory() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = null; | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage( | 
|  | String.format( | 
|  | "A file path must be given for the replacement of the robot comment on %s", | 
|  | withFixRobotCommentInput.path)); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .onlyReplacement() | 
|  | .range() | 
|  | .isEqualTo(fixReplacementInfo.range); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangeOfFixReplacementIsMandatory() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.range = null; | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage( | 
|  | String.format( | 
|  | "A range must be given for the replacement of the robot comment on %s", | 
|  | withFixRobotCommentInput.path)); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangeOfFixReplacementNeedsToBeValid() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.range = createRange(13, 9, 5, 10); | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage("Range (13:9 - 5:10)"); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap() | 
|  | throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 1); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Second modification\n"; | 
|  |  | 
|  | FixSuggestionInfo fixSuggestionInfo = | 
|  | createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2); | 
|  | withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage("overlap"); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap() | 
|  | throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 1); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME2; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Second modification\n"; | 
|  |  | 
|  | FixSuggestionInfo fixSuggestionInfo = | 
|  | createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2); | 
|  | withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(1); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap() | 
|  | throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 1); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Second modification\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2); | 
|  |  | 
|  | withFixRobotCommentInput.fixSuggestions = | 
|  | ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(2); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Second modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo3 = new FixReplacementInfo(); | 
|  | fixReplacementInfo3.path = FILE_NAME; | 
|  | fixReplacementInfo3.range = createRange(4, 0, 5, 0); | 
|  | fixReplacementInfo3.replacement = "Third modification\n"; | 
|  |  | 
|  | FixSuggestionInfo fixSuggestionInfo = | 
|  | createFixSuggestionInfo(fixReplacementInfo2, fixReplacementInfo1, fixReplacementInfo3); | 
|  | withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().replacements().hasSize(3); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  |  | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | assertThatList(robotCommentInfos) | 
|  | .onlyElement() | 
|  | .onlyFixSuggestion() | 
|  | .onlyReplacement() | 
|  | .replacement() | 
|  | .isEqualTo(fixReplacementInfo.replacement); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void replacementStringOfFixReplacementIsMandatory() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.replacement = null; | 
|  |  | 
|  | exception.expect(BadRequestException.class); | 
|  | exception.expectMessage( | 
|  | String.format( | 
|  | "A content for replacement must be " | 
|  | + "indicated for the replacement of the robot comment on %s", | 
|  | withFixRobotCommentInput.path)); | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixWithinALineCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n" | 
|  | + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixSpanningMultipleLinesCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content\n5"; | 
|  | fixReplacementInfo.range = createRange(3, 2, 5, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nSecond line\nThModified content\n5th line\nSixth line\nSeventh line\n" | 
|  | + "Eighth line\nNinth line\nTenth line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Some other modified content\n"; | 
|  |  | 
|  | FixSuggestionInfo fixSuggestionInfo = | 
|  | createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2); | 
|  | withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nFirst modification\nSome other modified content\nFourth line\nFifth line\n" | 
|  | + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void twoFixesOnSameFileCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(8, 0, 9, 0); | 
|  | fixReplacementInfo2.replacement = "Some other modified content\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2); | 
|  |  | 
|  | RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1); | 
|  | RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2); | 
|  | addRobotComment(changeId, robotCommentInput1); | 
|  | addRobotComment(changeId, robotCommentInput2); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(0)); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(1)); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n" | 
|  | + "Seventh line\nSome other modified content\nNinth line\nTenth line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void twoConflictingFixesOnSameFileCannotBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 1); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(3, 0, 4, 0); | 
|  | fixReplacementInfo2.replacement = "Some other modified content\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2); | 
|  |  | 
|  | RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1); | 
|  | RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2); | 
|  | addRobotComment(changeId, robotCommentInput1); | 
|  | addRobotComment(changeId, robotCommentInput2); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(0)); | 
|  | exception.expect(ResourceConflictException.class); | 
|  | exception.expectMessage("merge"); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(1)); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void twoFixesOfSameRobotCommentCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME; | 
|  | fixReplacementInfo2.range = createRange(8, 0, 9, 0); | 
|  | fixReplacementInfo2.replacement = "Some other modified content\n"; | 
|  | FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2); | 
|  |  | 
|  | withFixRobotCommentInput.fixSuggestions = | 
|  | ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(0)); | 
|  | gApi.changes().id(changeId).current().applyFix(fixIds.get(1)); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n" | 
|  | + "Seventh line\nSome other modified content\nNinth line\nTenth line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixReferringToDifferentFileThanRobotCommentCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME2; | 
|  | fixReplacementInfo.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo.replacement = "Modified content\n"; | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo("1st line\nModified content\n3rd line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixInvolvingTwoFilesCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo(); | 
|  | fixReplacementInfo1.path = FILE_NAME; | 
|  | fixReplacementInfo1.range = createRange(2, 0, 3, 0); | 
|  | fixReplacementInfo1.replacement = "First modification\n"; | 
|  |  | 
|  | FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo(); | 
|  | fixReplacementInfo2.path = FILE_NAME2; | 
|  | fixReplacementInfo2.range = createRange(1, 0, 2, 0); | 
|  | fixReplacementInfo2.replacement = "Different file modification\n"; | 
|  |  | 
|  | FixSuggestionInfo fixSuggestionInfo = | 
|  | createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2); | 
|  | withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n" | 
|  | + "Seventh line\nEighth line\nNinth line\nTenth line\n"); | 
|  | Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2); | 
|  | BinaryResultSubject.assertThat(file2) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo("Different file modification\n2nd line\n3rd line\n"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixReferringToNonExistentFileCannotBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = "a_non_existent_file.txt"; | 
|  | fixReplacementInfo.range = createRange(1, 0, 2, 0); | 
|  | fixReplacementInfo.replacement = "Modified content\n"; | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | exception.expect(ResourceNotFoundException.class); | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | // Remember patch set and add another one. | 
|  | String previousRevision = gApi.changes().id(changeId).get().currentRevision; | 
|  | amendChange(changeId); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | exception.expect(ResourceConflictException.class); | 
|  | exception.expectMessage("current"); | 
|  | gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | // Create an empty change edit. | 
|  | gApi.changes().id(changeId).edit().create(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | // Remember patch set and add another one. | 
|  | String previousRevision = gApi.changes().id(changeId).get().currentRevision; | 
|  | amendChange(changeId); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | EditInfo editInfo = gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId); | 
|  |  | 
|  | Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME); | 
|  | BinaryResultSubject.assertThat(file) | 
|  | .value() | 
|  | .asString() | 
|  | .isEqualTo( | 
|  | "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n" | 
|  | + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n"); | 
|  | assertThat(editInfo).baseRevision().isEqualTo(previousRevision); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied() | 
|  | throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | // Create an empty change edit. | 
|  | gApi.changes().id(changeId).edit().create(); | 
|  |  | 
|  | // Add another patch set. | 
|  | amendChange(changeId); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | exception.expect(ResourceConflictException.class); | 
|  | exception.expectMessage("based"); | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | String changeEditCommitMessage = "This is the commit message of the change edit.\n"; | 
|  | gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); | 
|  | assertThat(commitMessage).isEqualTo(changeEditCommitMessage); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void applyingFixTwiceIsIdempotent() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  | String expectedEditCommit = | 
|  | gApi.changes().id(changeId).edit().get().map(edit -> edit.commit.commit).orElse(""); | 
|  |  | 
|  | // Apply the fix again. | 
|  | gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<EditInfo> editInfo = gApi.changes().id(changeId).edit().get(); | 
|  | assertThat(editInfo).value().commit().commit().isEqualTo(expectedEditCommit); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void nonExistentFixCannotBeApplied() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  | String nonExistentFixId = fixId + "_non-existent"; | 
|  |  | 
|  | exception.expect(ResourceNotFoundException.class); | 
|  | gApi.changes().id(changeId).current().applyFix(nonExistentFixId); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void applyingFixReturnsEditInfoForCreatedChangeEdit() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get(); | 
|  | String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse(""); | 
|  | assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit); | 
|  | String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse(""); | 
|  | assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void applyingFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | gApi.changes().id(changeId).edit().create(); | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get(); | 
|  | String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse(""); | 
|  | assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit); | 
|  | String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse(""); | 
|  | assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void createdChangeEditIsBasedOnCurrentPatchSet() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  | String currentRevision = gApi.changes().id(changeId).get().currentRevision; | 
|  |  | 
|  | fixReplacementInfo.path = FILE_NAME; | 
|  | fixReplacementInfo.replacement = "Modified content"; | 
|  | fixReplacementInfo.range = createRange(3, 1, 3, 3); | 
|  |  | 
|  | addRobotComment(changeId, withFixRobotCommentInput); | 
|  | List<RobotCommentInfo> robotCommentInfos = getRobotComments(); | 
|  |  | 
|  | List<String> fixIds = getFixIds(robotCommentInfos); | 
|  | String fixId = Iterables.getOnlyElement(fixIds); | 
|  |  | 
|  | EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId); | 
|  |  | 
|  | assertThat(editInfo).baseRevision().isEqualTo(currentRevision); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void robotCommentsNotSupportedWithoutNoteDb() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isFalse(); | 
|  |  | 
|  | RobotCommentInput in = createRobotCommentInput(); | 
|  | ReviewInput reviewInput = new ReviewInput(); | 
|  | Map<String, List<RobotCommentInput>> robotComments = new HashMap<>(); | 
|  | robotComments.put(in.path, ImmutableList.of(in)); | 
|  | reviewInput.robotComments = robotComments; | 
|  | reviewInput.message = "comment test"; | 
|  |  | 
|  | exception.expect(MethodNotAllowedException.class); | 
|  | exception.expectMessage("robot comments not supported"); | 
|  | gApi.changes().id(changeId).current().review(reviewInput); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void queryChangesWithUnresolvedCommentCount() throws Exception { | 
|  | assume().that(notesMigration.readChanges()).isTrue(); | 
|  |  | 
|  | PushOneCommit.Result r1 = createChange(); | 
|  | PushOneCommit.Result r2 = | 
|  | pushFactory | 
|  | .create( | 
|  | db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId()) | 
|  | .to("refs/for/master"); | 
|  |  | 
|  | addRobotComment(r2.getChangeId(), createRobotCommentInputWithMandatoryFields()); | 
|  |  | 
|  | AcceptanceTestRequestScope.Context ctx = disableDb(); | 
|  | try { | 
|  | ChangeInfo result = Iterables.getOnlyElement(query(r2.getChangeId())); | 
|  | // currently, we create all robot comments as 'resolved' by default. | 
|  | // if we allow users to resolve a robot comment, then this test should | 
|  | // be modified. | 
|  | assertThat(result.unresolvedCommentCount).isEqualTo(0); | 
|  | } finally { | 
|  | enableDb(ctx); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static RobotCommentInput createRobotCommentInputWithMandatoryFields() { | 
|  | RobotCommentInput in = new RobotCommentInput(); | 
|  | in.robotId = "happyRobot"; | 
|  | in.robotRunId = "1"; | 
|  | in.line = 1; | 
|  | in.message = "nit: trailing whitespace"; | 
|  | in.path = FILE_NAME; | 
|  | return in; | 
|  | } | 
|  |  | 
|  | private static RobotCommentInput createRobotCommentInput( | 
|  | FixSuggestionInfo... fixSuggestionInfos) { | 
|  | RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); | 
|  | in.url = "http://www.happy-robot.com"; | 
|  | in.properties = new HashMap<>(); | 
|  | in.properties.put("key1", "value1"); | 
|  | in.properties.put("key2", "value2"); | 
|  | in.fixSuggestions = Arrays.asList(fixSuggestionInfos); | 
|  | return in; | 
|  | } | 
|  |  | 
|  | private static FixSuggestionInfo createFixSuggestionInfo( | 
|  | FixReplacementInfo... fixReplacementInfos) { | 
|  | FixSuggestionInfo newFixSuggestionInfo = new FixSuggestionInfo(); | 
|  | newFixSuggestionInfo.fixId = "An ID which must be overwritten."; | 
|  | newFixSuggestionInfo.description = "A description for a suggested fix."; | 
|  | newFixSuggestionInfo.replacements = Arrays.asList(fixReplacementInfos); | 
|  | return newFixSuggestionInfo; | 
|  | } | 
|  |  | 
|  | private static FixReplacementInfo createFixReplacementInfo() { | 
|  | FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo(); | 
|  | newFixReplacementInfo.path = FILE_NAME; | 
|  | newFixReplacementInfo.replacement = "some replacement code"; | 
|  | newFixReplacementInfo.range = createRange(3, 9, 8, 4); | 
|  | return newFixReplacementInfo; | 
|  | } | 
|  |  | 
|  | private static Comment.Range createRange( | 
|  | int startLine, int startCharacter, int endLine, int endCharacter) { | 
|  | Comment.Range range = new Comment.Range(); | 
|  | range.startLine = startLine; | 
|  | range.startCharacter = startCharacter; | 
|  | range.endLine = endLine; | 
|  | range.endCharacter = endCharacter; | 
|  | return range; | 
|  | } | 
|  |  | 
|  | private void addRobotComment(String targetChangeId, RobotCommentInput robotCommentInput) | 
|  | throws Exception { | 
|  | ReviewInput reviewInput = new ReviewInput(); | 
|  | reviewInput.robotComments = | 
|  | Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput)); | 
|  | reviewInput.message = "robot comment test"; | 
|  | gApi.changes().id(targetChangeId).current().review(reviewInput); | 
|  | } | 
|  |  | 
|  | private List<RobotCommentInfo> getRobotComments() throws RestApiException { | 
|  | return gApi.changes().id(changeId).current().robotCommentsAsList(); | 
|  | } | 
|  |  | 
|  | private void assertRobotComment(RobotCommentInfo c, RobotCommentInput expected) { | 
|  | assertRobotComment(c, expected, true); | 
|  | } | 
|  |  | 
|  | private void assertRobotComment( | 
|  | RobotCommentInfo c, RobotCommentInput expected, boolean expectPath) { | 
|  | assertThat(c.robotId).isEqualTo(expected.robotId); | 
|  | assertThat(c.robotRunId).isEqualTo(expected.robotRunId); | 
|  | assertThat(c.url).isEqualTo(expected.url); | 
|  | assertThat(c.properties).isEqualTo(expected.properties); | 
|  | assertThat(c.line).isEqualTo(expected.line); | 
|  | assertThat(c.message).isEqualTo(expected.message); | 
|  |  | 
|  | assertThat(c.author.email).isEqualTo(admin.email); | 
|  |  | 
|  | if (expectPath) { | 
|  | assertThat(c.path).isEqualTo(expected.path); | 
|  | } else { | 
|  | assertThat(c.path).isNull(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static String getStringFor(int numberOfBytes) { | 
|  | char[] chars = new char[numberOfBytes]; | 
|  | // 'a' will require one byte even when mapped to a JSON string | 
|  | Arrays.fill(chars, 'a'); | 
|  | return new String(chars); | 
|  | } | 
|  |  | 
|  | private static List<String> getFixIds(List<RobotCommentInfo> robotComments) { | 
|  | assertThatList(robotComments).isNotNull(); | 
|  | return robotComments.stream() | 
|  | .map(robotCommentInfo -> robotCommentInfo.fixSuggestions) | 
|  | .filter(Objects::nonNull) | 
|  | .flatMap(List::stream) | 
|  | .map(fixSuggestionInfo -> fixSuggestionInfo.fixId) | 
|  | .collect(toList()); | 
|  | } | 
|  | } |