blob: 4c8750af6be97876d504ac37f6fb9c542d2e4a83 [file] [log] [blame]
// Copyright (C) 2020 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.server.restapi.change;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Comparator.comparing;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.truth.Correspondence;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
import com.google.gerrit.server.restapi.change.CommentPorter.Metrics;
import com.google.gerrit.truth.NullAwareCorrespondence;
import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class CommentPorterTest {
private final ObjectId dummyObjectId =
ObjectId.fromString("0123456789012345678901234567890123456789");
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private DiffOperations diffOperations;
@Mock private CommentsUtil commentsUtil;
private static final CommentPorter.Metrics metrics = new Metrics(new DisabledMetricMaker());
private int uuidCounter = 0;
@Test
public void commentsAreNotDroppedWhenDiffNotAvailable() throws Exception {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
HumanComment comment = createComment(patchset1.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
any(Project.NameKey.class),
any(ObjectId.class),
any(ObjectId.class),
any(DiffOptions.class)))
.thenThrow(DiffNotAvailableException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset2, ImmutableList.of(comment), ImmutableList.of());
assertThat(portedComments).isNotEmpty();
}
@Test
public void commentsAreNotDroppedWhenDiffHasUnexpectedError() throws Exception {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
HumanComment comment = createComment(patchset1.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
any(Project.NameKey.class),
any(ObjectId.class),
any(ObjectId.class),
any(DiffOptions.class)))
.thenThrow(IllegalStateException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset2, ImmutableList.of(comment), ImmutableList.of());
assertThat(portedComments).isNotEmpty();
}
@Test
public void commentsAreNotDroppedWhenRetrievingCommitSha1sHasUnexpectedError() {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
HumanComment comment = createComment(patchset1.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenThrow(IllegalStateException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset2, ImmutableList.of(comment), ImmutableList.of());
assertThat(portedComments).isNotEmpty();
}
@Test
public void commentsAreMappedToPatchsetLevelOnDiffError() throws Exception {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
HumanComment comment = createComment(patchset1.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
any(Project.NameKey.class),
any(ObjectId.class),
any(ObjectId.class),
any(DiffOptions.class)))
.thenThrow(IllegalStateException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset2, ImmutableList.of(comment), ImmutableList.of());
assertThat(portedComments)
.comparingElementsUsing(hasFilePath())
.containsExactly(Patch.PATCHSET_LEVEL);
}
@Test
public void commentsAreStillPortedWhenDiffOfOtherCommentsHasError() throws Exception {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
PatchSet patchset3 = createPatchset(PatchSet.id(changeId, 3));
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset1, patchset2, patchset3);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
// Place the comments on different patchsets to have two different diff requests.
HumanComment comment1 = createComment(patchset1.id(), "myFile");
HumanComment comment2 = createComment(patchset2.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
// Throw an exception on the first diff request but return an actual value on the second.
when(diffOperations.listModifiedFiles(
any(Project.NameKey.class),
any(ObjectId.class),
any(ObjectId.class),
any(DiffOptions.class)))
.thenThrow(IllegalStateException.class)
.thenReturn(ImmutableMap.of());
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset3, ImmutableList.of(comment1, comment2), ImmutableList.of());
// One of the comments should still be ported as usual. -> Keeps its file name as the diff was
// empty.
assertThat(portedComments).comparingElementsUsing(hasFilePath()).contains("myFile");
}
@Test
public void commentsWithInvalidPatchsetsAreIgnored() throws Exception {
Project.NameKey project = Project.nameKey("myProject");
Change.Id changeId = Change.id(1);
Change change = createChange(project, changeId);
PatchSet patchset1 = createPatchset(PatchSet.id(changeId, 1));
PatchSet patchset2 = createPatchset(PatchSet.id(changeId, 2));
// Leave out patchset 1 (e.g. reserved for draft patchsets in the past).
ChangeNotes changeNotes = mockChangeNotes(project, change, patchset2);
CommentPorter commentPorter = new CommentPorter(diffOperations, commentsUtil, metrics);
HumanComment comment = createComment(patchset1.id(), "myFile");
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
any(Project.NameKey.class),
any(ObjectId.class),
any(ObjectId.class),
any(DiffOptions.class)))
.thenReturn(ImmutableMap.of());
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
changeNotes, patchset2, ImmutableList.of(comment), ImmutableList.of());
assertThat(portedComments).isEmpty();
}
private Change createChange(Project.NameKey project, Change.Id changeId) {
return new Change(
Change.key("changeKey"),
changeId,
Account.id(123),
BranchNameKey.create(project, "myBranch"),
Instant.ofEpochMilli(12345));
}
private PatchSet createPatchset(PatchSet.Id id) {
return PatchSet.builder()
.id(id)
.commitId(dummyObjectId)
.uploader(Account.id(123))
.createdOn(Instant.ofEpochMilli(12345))
.build();
}
private ChangeNotes mockChangeNotes(
Project.NameKey project, Change change, PatchSet... patchsets) {
ChangeNotes changeNotes = mock(ChangeNotes.class);
when(changeNotes.getProjectName()).thenReturn(project);
when(changeNotes.getChange()).thenReturn(change);
when(changeNotes.getChangeId()).thenReturn(change.getId());
ImmutableSortedMap<PatchSet.Id, PatchSet> sortedPatchsets =
Arrays.stream(patchsets)
.collect(
ImmutableSortedMap.toImmutableSortedMap(
comparing(PatchSet.Id::get), PatchSet::id, patchset -> patchset));
when(changeNotes.getPatchSets()).thenReturn(sortedPatchsets);
return changeNotes;
}
private HumanComment createComment(PatchSet.Id patchsetId, String filePath) {
return new HumanComment(
new Comment.Key(getUniqueUuid(), filePath, patchsetId.get()),
Account.id(100),
Instant.ofEpochMilli(1234),
(short) 1,
"Comment text",
"serverId",
true);
}
private String getUniqueUuid() {
return "commentUuid" + uuidCounter++;
}
private Correspondence<HumanComment, String> hasFilePath() {
return NullAwareCorrespondence.transforming(comment -> comment.key.filename, "hasFilePath");
}
}