blob: a2765d93cd2ef5e6a9a43d3ab6ec1daaf1dae420 [file] [log] [blame]
// Copyright (C) 2021 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.server.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.ContextLineInfo;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class CommentContextIT extends AbstractDaemonTest {
/** The commit message of a single commit. */
private static final String SUBJECT =
String.join(
"\n",
"Commit Header",
"",
"This commit is doing something extremely important",
"",
"Footer: value");
private static final String FILE_CONTENT =
String.join("\n", "Line 1 of file", "", "Line 3 of file", "", "", "Line 6 of file");
private static final ObjectId dummyCommit =
ObjectId.fromString("93e2901bc0b4719ef6081ee6353b49c9cdd97614");
@Inject private RequestScopeOperations requestScopeOperations;
@Before
public void setup() throws Exception {
requestScopeOperations.setApiUser(user.id());
}
@Test
public void commentContextForGitSubmoduleFiles() throws Exception {
String submodulePath = "submodule_path";
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo).addGitSubmodule(submodulePath, dummyCommit);
PushOneCommit.Result pushResult = push.to("refs/for/master");
String changeId = pushResult.getChangeId();
CommentInput comment =
CommentsUtil.newComment(submodulePath, Side.REVISION, 1, "comment", false);
CommentsUtil.addComments(gApi, changeId, pushResult.getCommit().name(), comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).path).isEqualTo(submodulePath);
assertThat(comments.get(0).contextLines)
.isEqualTo(createContextLines("1", "Subproject commit " + dummyCommit.getName()));
}
@Test
public void commentContextForRootCommitOnParentSideReturnsEmptyContext() throws Exception {
// Create a change in a new branch, making the patchset commit a root commit
ChangeInfo changeInfo = createChangeInNewBranch("newBranch");
String changeId = changeInfo.changeId;
String revision = changeInfo.revisions.keySet().iterator().next();
// Write a comment on the parent side of the commit message. Set parent=1 because if unset, our
// handler in PostReview assumes we want to write on the auto-merge commit and fails the
// pre-condition.
CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, Side.PARENT, 0, "comment", false);
comment.parent = 1;
CommentsUtil.addComments(gApi, changeId, revision, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
CommentInfo c = comments.stream().collect(MoreCollectors.onlyElement());
assertThat(c.commitId).isEqualTo(ObjectId.zeroId().name());
assertThat(c.contextLines).isEmpty();
}
@Test
public void commentContextForCommitMessageForLineComment() throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, Side.REVISION, 7, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
// The first few lines of the commit message are the headers, e.g.
// Parent: ...
// Author: ...
// AuthorDate: ...
// etc...
assertThat(comments.get(0).contextLines)
.containsExactlyElementsIn(createContextLines("7", "Commit Header"));
}
@Test
public void commentContextForMergeList() throws Exception {
PushOneCommit.Result result = createMergeCommitChange("refs/for/master");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment = CommentsUtil.newComment(MERGE_LIST, Side.REVISION, 1, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).contextLines)
.containsExactlyElementsIn(createContextLines("1", "Merge List:"));
}
@Test
public void commentContextForCommitMessageForRangeComment() throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment =
CommentsUtil.newComment(
COMMIT_MSG, Side.REVISION, createCommentRange(7, 9), "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
// The first few lines of the commit message are the headers, e.g.
// Parent: ...
// Author: ...
// AuthorDate: ...
// etc...
assertThat(comments.get(0).contextLines)
.containsExactlyElementsIn(
createContextLines(
"7",
"Commit Header",
"8",
"",
"9",
"This commit is doing something extremely important"));
}
@Test
public void commentContextForCommitMessageInvalidLine() throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment =
CommentsUtil.newComment(COMMIT_MSG, Side.REVISION, 100, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).contextLines).isEmpty();
}
@Test
public void listChangeCommentsWithContextEnabled() throws Exception {
PushOneCommit.Result r1 = createChange();
ImmutableList.Builder<String> content = ImmutableList.builder();
for (int i = 1; i <= 10; i++) {
content.add("line_" + i);
}
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
FILE_NAME,
content.build().stream().collect(Collectors.joining("\n")),
r1.getChangeId())
.to("refs/for/master");
CommentsUtil.addCommentOnLine(gApi, r2, "nit: please fix", 1);
CommentsUtil.addCommentOnRange(gApi, r2, "looks good", createCommentRange(2, 5));
List<CommentInfo> comments =
gApi.changes().id(r2.getChangeId()).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(2);
assertThat(
comments.stream()
.filter(c -> c.message.equals("nit: please fix"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(createContextLines("1", "line_1"));
assertThat(
comments.stream()
.filter(c -> c.message.equals("looks good"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(
createContextLines("2", "line_2", "3", "line_3", "4", "line_4", "5", "line_5"));
}
@Test
public void listChangeDraftsWithContextEnabled() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
FILE_NAME,
"line_1\nline_2\nline_3",
r1.getChangeId())
.to("refs/for/master");
DraftInput in = CommentsUtil.newDraft(FILE_NAME, Side.REVISION, 2, "comment 1");
gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).createDraft(in);
// Test the getAsList interface
List<CommentInfo> comments =
gApi.changes().id(r2.getChangeId()).draftsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).message).isEqualTo("comment 1");
assertThat(comments.get(0).contextLines)
.containsExactlyElementsIn(createContextLines("2", "line_2"));
// Also test the get interface
Map<String, List<CommentInfo>> commentsMap =
gApi.changes().id(r2.getChangeId()).draftsRequest().withContext(true).get();
assertThat(commentsMap).hasSize(1);
assertThat(commentsMap.values().iterator().next()).hasSize(1);
CommentInfo onlyComment = commentsMap.values().iterator().next().get(0);
assertThat(onlyComment.message).isEqualTo("comment 1");
assertThat(onlyComment.contextLines)
.containsExactlyElementsIn(createContextLines("2", "line_2"));
}
@Test
public void commentContextForCommentsOnDifferentPatchsets() throws Exception {
PushOneCommit.Result r1 = createChange();
ImmutableList.Builder<String> content = ImmutableList.builder();
for (int i = 1; i <= 10; i++) {
content.add("line_" + i);
}
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
FILE_NAME,
String.join("\n", content.build()),
r1.getChangeId())
.to("refs/for/master");
PushOneCommit.Result r3 =
pushFactory
.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
FILE_NAME,
content.build().stream().collect(Collectors.joining("\n")),
r1.getChangeId())
.to("refs/for/master");
CommentsUtil.addCommentOnLine(gApi, r2, "r2: please fix", 1);
CommentsUtil.addCommentOnRange(gApi, r2, "r2: looks good", createCommentRange(2, 3));
CommentsUtil.addCommentOnLine(gApi, r3, "r3: please fix", 6);
CommentsUtil.addCommentOnRange(gApi, r3, "r3: looks good", createCommentRange(7, 8));
List<CommentInfo> comments =
gApi.changes().id(r2.getChangeId()).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(4);
assertThat(
comments.stream()
.filter(c -> c.message.equals("r2: please fix"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(createContextLines("1", "line_1"));
assertThat(
comments.stream()
.filter(c -> c.message.equals("r2: looks good"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(createContextLines("2", "line_2", "3", "line_3"));
assertThat(
comments.stream()
.filter(c -> c.message.equals("r3: please fix"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(createContextLines("6", "line_6"));
assertThat(
comments.stream()
.filter(c -> c.message.equals("r3: looks good"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(createContextLines("7", "line_7", "8", "line_8"));
}
@Test
public void commentContextIsEmptyForPatchsetLevelComments() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment =
CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).contextLines).isEmpty();
}
@Test
public void commentContextWithZeroPadding() throws Exception {
String changeId = createChangeWithComment(3, 4);
assertContextLines(changeId, /* contextPadding= */ 0, ImmutableList.of(3, 4));
}
@Test
public void commentContextWithSmallPadding() throws Exception {
String changeId = createChangeWithComment(3, 4);
assertContextLines(changeId, /* contextPadding= */ 1, ImmutableList.of(2, 3, 4, 5));
}
@Test
public void commentContextWithSmallPaddingAtTheBeginningOfFile() throws Exception {
String changeId = createChangeWithComment(1, 2);
assertContextLines(changeId, /* contextPadding= */ 2, ImmutableList.of(1, 2, 3, 4));
}
@Test
public void commentContextWithPaddingLargerThanFileSize() throws Exception {
String changeId = createChangeWithComment(3, 3);
assertContextLines(
changeId,
/* contextPadding= */ 20,
ImmutableList.of(1, 2, 3, 4, 5, 6)); // file only contains six lines.
}
@Test
public void commentContextWithLargePaddingReturnsAdjustedMaximumPadding() throws Exception {
String changeId = createChangeWithCommentLarge(250, 250);
assertContextLines(
changeId,
/* contextPadding= */ 300,
IntStream.range(200, 301).boxed().collect(ImmutableList.toImmutableList()));
}
@Test
public void commentContextReturnsCorrectContentTypeForCommitMessage() throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, Side.REVISION, 7, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).path).isEqualTo(COMMIT_MSG);
assertThat(comments.get(0).sourceContentType)
.isEqualTo(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE);
}
@Test
public void commentContextReturnsCorrectContentType_java() throws Exception {
String javaContent =
"public class Main {\n"
+ " public static void main(String[]args){\n"
+ " if(args==null){\n"
+ " System.err.println(\"Something\");\n"
+ " }\n"
+ " }\n"
+ " }";
String fileName = "src.java";
String changeId = createChangeWithContent(fileName, javaContent, /* line= */ 4);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).path).isEqualTo(fileName);
assertThat(comments.get(0).contextLines)
.isEqualTo(createContextLines("4", " System.err.println(\"Something\");"));
assertThat(comments.get(0).sourceContentType).isEqualTo("text/x-java");
}
@Test
public void commentContextReturnsCorrectContentType_cpp() throws Exception {
String cppContent =
"#include <iostream>\n"
+ "\n"
+ "int main() {\n"
+ " std::cout << \"Hello World!\";\n"
+ " return 0;\n"
+ "}";
String fileName = "src.cpp";
String changeId = createChangeWithContent(fileName, cppContent, /* line= */ 4);
List<CommentInfo> comments =
gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(1);
assertThat(comments.get(0).path).isEqualTo(fileName);
assertThat(comments.get(0).contextLines)
.isEqualTo(createContextLines("4", " std::cout << \"Hello World!\";"));
assertThat(comments.get(0).sourceContentType).isEqualTo("text/x-c++src");
}
@Test
public void listChangeCommentsWithContextEnabled_twoRangeCommentsWithTheSameContext()
throws Exception {
PushOneCommit.Result r1 = createChange();
ImmutableList.Builder<String> content = ImmutableList.builder();
for (int i = 1; i <= 10; i++) {
content.add("line_" + i);
}
PushOneCommit.Result r2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
FILE_NAME,
content.build().stream().collect(Collectors.joining("\n")),
r1.getChangeId())
.to("refs/for/master");
CommentsUtil.addCommentOnRange(gApi, r2, "looks good", createCommentRange(2, 5));
CommentsUtil.addCommentOnRange(gApi, r2, "are you sure?", createCommentRange(2, 5));
List<CommentInfo> comments =
gApi.changes().id(r2.getChangeId()).commentsRequest().withContext(true).getAsList();
assertThat(comments).hasSize(2);
assertThat(
comments.stream()
.filter(c -> c.message.equals("looks good"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(
createContextLines("2", "line_2", "3", "line_3", "4", "line_4", "5", "line_5"));
assertThat(
comments.stream()
.filter(c -> c.message.equals("are you sure?"))
.collect(MoreCollectors.onlyElement())
.contextLines)
.containsExactlyElementsIn(
createContextLines("2", "line_2", "3", "line_3", "4", "line_4", "5", "line_5"));
}
private String createChangeWithContent(String fileName, String fileContent, int line)
throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, fileName, fileContent, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
CommentInput comment = CommentsUtil.newComment(fileName, Side.REVISION, line, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
return changeId;
}
private String createChangeWithComment(int startLine, int endLine) throws Exception {
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
Comment.Range commentRange = createCommentRange(startLine, endLine);
CommentInput comment =
CommentsUtil.newComment(FILE_NAME, Side.REVISION, commentRange, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
return changeId;
}
private String createChangeWithCommentLarge(int startLine, int endLine) throws Exception {
StringBuilder largeContent = new StringBuilder();
for (int i = 0; i < 1000; i++) {
largeContent.append("line " + i + "\n");
}
PushOneCommit.Result result =
createChange(testRepo, "master", SUBJECT, FILE_NAME, largeContent.toString(), "topic");
String changeId = result.getChangeId();
String ps1 = result.getCommit().name();
Comment.Range commentRange = createCommentRange(startLine, endLine);
CommentInput comment =
CommentsUtil.newComment(FILE_NAME, Side.REVISION, commentRange, "comment", false);
CommentsUtil.addComments(gApi, changeId, ps1, comment);
return changeId;
}
private void assertContextLines(
String changeId, int contextPadding, ImmutableList<Integer> expectedLines) throws Exception {
List<CommentInfo> comments =
gApi.changes()
.id(changeId)
.commentsRequest()
.withContext(true)
.contextPadding(contextPadding)
.getAsList();
assertThat(comments).hasSize(1);
assertThat(
comments.get(0).contextLines.stream()
.map(c -> c.lineNumber)
.collect(Collectors.toList()))
.containsExactlyElementsIn(expectedLines);
}
private Comment.Range createCommentRange(int startLine, int endLine) {
Comment.Range range = new Comment.Range();
range.startLine = startLine;
range.endLine = endLine;
return range;
}
private List<ContextLineInfo> createContextLines(String... args) {
List<ContextLineInfo> result = new ArrayList<>();
for (int i = 0; i < args.length; i += 2) {
int lineNbr = Integer.parseInt(args[i]);
String contextLine = args[i + 1];
ContextLineInfo info = new ContextLineInfo(lineNbr, contextLine);
result.add(info);
}
return result;
}
private ChangeInfo createChangeInNewBranch(String branchName) throws Exception {
ChangeInput in = new ChangeInput();
in.project = project.get();
in.branch = branchName;
in.newBranch = true;
in.subject = "New changes";
return gApi.changes().create(in).get();
}
}