blob: 822807e0606ef85c85d4f89b8ac40357f35722a7 [file] [log] [blame]
// Copyright (C) 2022 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.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.codeReview;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static org.eclipse.jgit.lib.Constants.HEAD;
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.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.TestChange;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.util.Map;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
@UseTimezone(timezone = "US/Eastern")
@VerifyNoPiiInChangeNotes(true)
public class SubmitRequirementPredicateIT extends AbstractDaemonTest {
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private SubmitRequirementsEvaluatorImpl submitRequirementsEvaluator;
@Inject private ChangeOperations changeOperations;
@Inject private ProjectOperations projectOperations;
@Inject private AccountOperations accountOperations;
private final LabelType label =
label("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
private final LabelType pLabel =
label("Custom-Label2", value(1, "Positive"), value(0, "No score"));
@Before
public void setUp() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(label.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
.add(allowLabel(pLabel.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(0, 1))
.update();
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(label);
u.getConfig().upsertLabelType(pLabel);
u.save();
}
}
@Test
public void labelVote_greaterThan_withManyMaxVotes() throws Exception {
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
PushOneCommit.Result r1 =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
Account.Id user11 = accountCreator.create("user11").id();
Account.Id user12 = accountCreator.create("user12").id();
Account.Id user13 = accountCreator.create("user13").id();
Account.Id user14 = accountCreator.create("user14").id();
Account.Id user15 = accountCreator.create("user15").id();
Account.Id user16 = accountCreator.create("user16").id();
Account.Id user17 = accountCreator.create("user17").id();
ImmutableList<Account.Id> allUsers =
ImmutableList.of(user11, user12, user13, user14, user15, user16, user17);
// Give voting permissions to all users
requestScopeOperations.setApiUser(admin.id());
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// The predicate uses the MAX_COUNT_INTERNAL in label predicate, and the SR expression matches
// even if the change has more than 5 votes.
for (Account.Id aId : allUsers) {
approveAsUser(r1.getChangeId(), aId);
assertMatching("label:Code-Review=+2,count>=1", r1.getChange().getId());
}
}
@Test
public void messagePredicate_ignoresPunctuationPreservesOrder() throws Exception {
Change.Id c1 =
changeOperations
.newChange()
.commitMessage("Hello Earth, from planet Mars")
.project(project)
.createV1();
// The punctuation and capitalisation is ignored.
assertMatching("message:\"earth from planet\"", c1);
// The punctuation and capitalisation is ignored.
assertNotMatching("message:\"planet from earth\"", c1);
}
@Test
public void distinctVoters_sameUserVotesOnDifferentLabels_fails() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(admin.id());
approve(c1.toString());
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
// Same user votes on both labels
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", 1));
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
}
@Test
public void distinctVoters_distinctUsersOnDifferentLabels_passes() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(admin.id());
approve(c1.toString());
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", 1));
assertMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
}
@Test
public void distinctVoters_onlyMaxVotesRespected() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", 1));
requestScopeOperations.setApiUser(admin.id());
recommend(c1.toString());
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
requestScopeOperations.setApiUser(admin.id());
approve(c1.toString());
assertMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MAX,count>1\"", c1);
}
@Test
public void distinctVoters_onlyMinVotesRespected() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", -1));
requestScopeOperations.setApiUser(admin.id());
recommend(c1.toString());
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MIN,count>1\"", c1);
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(c1.toString()).current().review(ReviewInput.reject());
assertMatching("distinctvoters:\"[Code-Review,Custom-Label],value=MIN,count>1\"", c1);
}
@Test
public void distinctVoters_onlyExactValueRespected() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", 1));
requestScopeOperations.setApiUser(admin.id());
approve(c1.toString());
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],value=1,count>1\"", c1);
requestScopeOperations.setApiUser(admin.id());
recommend(c1.toString());
assertMatching("distinctvoters:\"[Code-Review,Custom-Label],value=1,count>1\"", c1);
}
@Test
public void distinctVoters_valueIsOptional() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", -1));
requestScopeOperations.setApiUser(admin.id());
assertNotMatching("distinctvoters:\"[Code-Review,Custom-Label],count>1\"", c1);
recommend(c1.toString());
assertMatching("distinctvoters:\"[Code-Review,Custom-Label],count>1\"", c1);
}
@Test
public void distinctVoters_moreThanTwoLabels() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label2", 1));
requestScopeOperations.setApiUser(admin.id());
recommend(c1.toString());
assertMatching(
"distinctvoters:\"[Code-Review,Custom-Label,Custom-Label2],value=1,count>1\"", c1);
}
@Test
public void distinctVoters_moreThanTwoLabels_moreThanTwoUsers() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).createV1();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label2", 1));
requestScopeOperations.setApiUser(admin.id());
recommend(c1.toString());
assertNotMatching(
"distinctvoters:\"[Code-Review,Custom-Label,Custom-Label2],value=1,count>2\"", c1);
Account.Id tester = accountOperations.newAccount().create();
requestScopeOperations.setApiUser(tester);
gApi.changes()
.id(c1.toString())
.current()
.review(ReviewInput.create().label("Custom-Label", 1));
assertMatching(
"distinctvoters:\"[Code-Review,Custom-Label,Custom-Label2],value=1,count>2\"", c1);
}
@Test
public void hasSubmoduleUpdate_withSubmoduleChangeInParent1() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result r1 = createGitSubmoduleCommit("refs/for/master");
testRepo.reset(initial);
PushOneCommit.Result r2 = createNormalCommit("refs/for/master", "file1");
PushOneCommit.Result merge =
createMergeCommitChange(
"refs/for/master",
r1.getCommit(),
r2.getCommit(),
mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
assertNotMatching("has:submodule-update,base=1", merge.getChange().getId());
assertMatching("has:submodule-update,base=2", merge.getChange().getId());
assertNotMatching("has:submodule-update", merge.getChange().getId());
}
@Test
public void hasSubmoduleUpdate_withSubmoduleChangeInParent2() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
testRepo.reset(initial);
PushOneCommit.Result r2 = createGitSubmoduleCommit("refs/for/master");
PushOneCommit.Result merge =
createMergeCommitChange(
"refs/for/master",
r1.getCommit(),
r2.getCommit(),
mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
assertMatching("has:submodule-update,base=1", merge.getChange().getId());
assertNotMatching("has:submodule-update,base=2", merge.getChange().getId());
assertNotMatching("has:submodule-update", merge.getChange().getId());
}
@Test
public void hasSubmoduleUpdate_withoutSubmoduleChange_doesNotMatch() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
testRepo.reset(initial);
PushOneCommit.Result r2 = createNormalCommit("refs/for/master", "file2");
PushOneCommit.Result merge =
createMergeCommitChange(
"refs/for/master",
r1.getCommit(),
r2.getCommit(),
mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
assertNotMatching("has:submodule-update,base=1", merge.getChange().getId());
assertNotMatching("has:submodule-update,base=2", merge.getChange().getId());
assertNotMatching("has:submodule-update", merge.getChange().getId());
}
@Test
public void hasSubmoduleUpdate_withBaseParamGreaterThanParentCount_doesNotMatch()
throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result r1 = createNormalCommit("refs/for/master", "file1");
testRepo.reset(initial);
PushOneCommit.Result r2 = createGitSubmoduleCommit("refs/for/master");
PushOneCommit.Result merge =
createMergeCommitChange(
"refs/for/master",
r1.getCommit(),
r2.getCommit(),
mergeAndGetTreeId(r1.getCommit(), r2.getCommit()));
assertNotMatching("has:submodule-update,base=3", merge.getChange().getId());
}
@Test
public void hasSubmoduleUpdate_withWrongArgs_throws() {
assertError(
"has:submodule-update,base=xyz",
changeOperations.newChange().project(project).createV1(),
"failed to parse the parent number xyz: For input string: \"xyz\"");
assertError(
"has:submodule-update,base=1,arg=foo",
changeOperations.newChange().project(project).createV1(),
"wrong number of arguments for the has:submodule-update operator");
assertError(
"has:submodule-update,base",
changeOperations.newChange().project(project).createV1(),
"unexpected base value format");
}
@Test
public void nonContributorLabelVote_match() throws Exception {
requestScopeOperations.setApiUser(user.id());
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
PushOneCommit.Result r1 =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
Change.Id cId = r1.getChange().getId();
ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
// Assert on uploader, committer and author
assertUploader(changeInfo, user.email());
assertCommitter(changeInfo, user.email());
assertAuthor(changeInfo, user.email());
// Vote from admin (a.k.a. non uploader/committer/author) matches
requestScopeOperations.setApiUser(admin.id());
approve(cId.toString());
assertMatching("label:Code-Review=+2,user=non_contributor", cId);
// Also make sure magic label votes and > operator work
assertMatching("label:Code-Review=MAX,user=non_contributor", cId);
assertMatching("label:Code-Review>+1,user=non_contributor", cId);
}
@Test
public void nonContributorLabelVote_voteFromUploader_doesNotMatch() throws Exception {
PushOneCommit.Result r1 = createNormalCommit(user.newIdent(), "refs/for/master", "file1");
ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
assertUploader(changeInfo, admin.email());
// Vote from admin (a.k.a. uploader) does not match
requestScopeOperations.setApiUser(admin.id());
approve(r1.getChangeId());
assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
}
@Test
@Sandboxed
public void nonContributorLabelVote_voteFromAuthor_doesNotMatch() throws Exception {
Account.Id authorId =
accountOperations
.newAccount()
.fullname("author")
.preferredEmail("authoremail@example.com")
.create();
Account.Id committerId =
accountOperations
.newAccount()
.fullname("committer")
.preferredEmail("committeremail@example.com")
.create();
Change.Id changeId =
changeOperations
.newChange()
.project(project)
.author(authorId)
.committer(committerId)
.createV1();
ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
assertAuthor(changeInfo, "authoremail@example.com");
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// Vote from author does not match
requestScopeOperations.setApiUser(authorId);
approve(changeId.toString());
assertNotMatching("label:Code-Review=+2,user=non_contributor", changeId);
}
@Test
public void nonContributorLabelVote_voteFromCommitter_doesNotMatch() throws Exception {
Account.Id authorId =
accountOperations
.newAccount()
.fullname("author")
.preferredEmail("authoremail@example.com")
.create();
Account.Id committerId =
accountOperations
.newAccount()
.fullname("committer")
.preferredEmail("committeremail@example.com")
.create();
Change.Id changeId =
changeOperations
.newChange()
.project(project)
.author(authorId)
.committer(committerId)
.createV1();
ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
assertCommitter(changeInfo, "committeremail@example.com");
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// Vote from committer does not match
requestScopeOperations.setApiUser(committerId);
approve(changeId.toString());
assertNotMatching("label:Code-Review=+2,user=non_contributor", changeId);
}
@Test
public void nonContributorLabelVote_uploaderAndAuthorDifferent() throws Exception {
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
PushOneCommit.Result r1 =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
requestScopeOperations.setApiUser(admin.id());
ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
assertUploader(changeInfo, admin.email());
assertAuthor(changeInfo, user.email());
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// Vote from admin (a.k.a. uploader) does not match
requestScopeOperations.setApiUser(user.id());
approve(r1.getChangeId());
assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
// Vote from user (a.k.a. author) does not match
requestScopeOperations.setApiUser(admin.id());
approve(r1.getChangeId());
assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
// Vote from user2 (a.k.a. non-author and non-uploader) matches
TestAccount user2 = accountCreator.create();
requestScopeOperations.setApiUser(user2.id());
approve(r1.getChangeId());
assertMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
}
@Test
public void nonAuthorLabelVote_match() throws Exception {
requestScopeOperations.setApiUser(user.id());
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
PushOneCommit.Result r1 =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
Change.Id cId = r1.getChange().getId();
ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
// Assert on author
assertAuthor(changeInfo, user.email());
// Vote from admin (a.k.a. non author) matches
requestScopeOperations.setApiUser(admin.id());
approve(cId.toString());
assertMatching("label:Code-Review=+2,user=non_author", cId);
// Also make sure magic label votes and > operator work
assertMatching("label:Code-Review=MAX,user=non_author", cId);
assertMatching("label:Code-Review>+1,user=non_author", cId);
}
@Test
@Sandboxed
public void nonAuthorLabelVote_voteFromAuthor_doesNotMatch() throws Exception {
Account.Id authorId =
accountOperations
.newAccount()
.fullname("author")
.preferredEmail("authoremail@example.com")
.create();
Account.Id committerId =
accountOperations
.newAccount()
.fullname("committer")
.preferredEmail("committeremail@example.com")
.create();
TestChange change =
changeOperations
.newChange()
.project(project)
.author(authorId)
.committer(committerId)
.createAndGet();
ChangeInfo changeInfo = gApi.changes().id(change.id()).get();
assertAuthor(changeInfo, "authoremail@example.com");
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// Vote from author does not match
requestScopeOperations.setApiUser(authorId);
approve(change.id().id());
assertNotMatching("label:Code-Review=+2,user=non_author", change.numericChangeId());
}
@Test
public void nonCommitterLabelVote_voteFromCommitter_doesNotMatch() throws Exception {
Account.Id authorId =
accountOperations
.newAccount()
.fullname("author")
.preferredEmail("authoremail@example.com")
.create();
Account.Id committerId =
accountOperations
.newAccount()
.fullname("committer")
.preferredEmail("committeremail@example.com")
.create();
TestChange change =
changeOperations
.newChange()
.project(project)
.author(authorId)
.committer(committerId)
.createAndGet();
ChangeInfo changeInfo = gApi.changes().id(change.id()).get();
assertCommitter(changeInfo, "committeremail@example.com");
allowLabelPermission(
codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
// Vote from committer does not match
requestScopeOperations.setApiUser(committerId);
approve(change.id().id());
assertNotMatching("label:Code-Review=+2,user=non_committer", change.numericChangeId());
}
@Test
public void nonCommitterLabelVote_match() throws Exception {
requestScopeOperations.setApiUser(user.id());
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
PushOneCommit.Result r1 =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
Change.Id cId = r1.getChange().getId();
ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
// Assert on committer
assertAuthor(changeInfo, user.email());
// Vote from admin (a.k.a. non committer) matches
requestScopeOperations.setApiUser(admin.id());
approve(cId.toString());
assertMatching("label:Code-Review=+2,user=non_committer", cId);
// Also make sure magic label votes and > operator work
assertMatching("label:Code-Review=MAX,user=non_committer", cId);
assertMatching("label:Code-Review>+1,user=non_committer", cId);
}
@Test
public void label_requireVoteFromHumanReviewers() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allowLabel("Code-Review").ref("refs/*").group(REGISTERED_USERS).range(-2, 2))
.update();
Account.Id owner = accountCreator.create("owner").id();
Account.Id reviewer1 = accountCreator.create("reviewer1").id();
Account.Id reviewer2 = accountCreator.create("reviewer2").id();
Account.Id reviewer3 = accountCreator.create("reviewer3").id();
Account.Id serviceUser = accountCreator.create("serviceUser").id();
gApi.groups().id(ServiceUserClassifier.SERVICE_USERS).addMembers(serviceUser.toString());
Change.Id changeApprovedByAllReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(project, changeApprovedByAllReviewers, reviewer1, reviewer2, reviewer3);
addReviews(
project,
changeApprovedByAllReviewers,
ReviewInput.approve(),
reviewer1,
reviewer2,
reviewer3);
Change.Id changeApprovedBySomeReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(project, changeApprovedBySomeReviewers, reviewer1, reviewer2, reviewer3);
addReviews(project, changeApprovedBySomeReviewers, ReviewInput.approve(), reviewer1, reviewer2);
Change.Id changeRecommendedByAllReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(project, changeRecommendedByAllReviewers, reviewer1, reviewer2, reviewer3);
addReviews(
project,
changeRecommendedByAllReviewers,
ReviewInput.recommend(),
reviewer1,
reviewer2,
reviewer3);
Change.Id changeRecommendedBySomeReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(project, changeRecommendedBySomeReviewers, reviewer1, reviewer2, reviewer3);
addReviews(
project, changeRecommendedBySomeReviewers, ReviewInput.recommend(), reviewer1, reviewer2);
Change.Id changeNoVotesByReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(project, changeNoVotesByReviewers, reviewer1, reviewer2, reviewer3);
Change.Id changeWithoutReviewers =
changeOperations.newChange().project(project).owner(owner).createV1();
requestScopeOperations.setApiUser(user.id());
// change without reviewers doesn't match
assertNotMatching("label:Code-Review=MAX,users=human_reviewers", changeWithoutReviewers);
// match changes where all reviewers have the same vote
assertRequirement(
"label:Code-Review=MAX,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
assertRequirement(
"label:Code-Review=2,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
assertRequirement(
"label:Code-Review=1,users=human_reviewers",
ImmutableList.of(changeRecommendedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeApprovedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// match changes where no reviewer voted (same as "label:Code-Review=0")
assertRequirement(
"label:Code-Review=0,users=human_reviewers",
ImmutableList.of(changeNoVotesByReviewers),
ImmutableList.of(
changeApprovedByAllReviewers,
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers));
// match changes where all reviewers have a vote <=, >=, < or >
assertRequirement(
"label:Code-Review<=2,users=human_reviewers",
ImmutableList.of(
changeApprovedByAllReviewers,
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers),
ImmutableList.of());
assertRequirement(
"label:Code-Review<=1,users=human_reviewers",
ImmutableList.of(
changeRecommendedByAllReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers),
ImmutableList.of(changeApprovedByAllReviewers));
assertRequirement(
"label:Code-Review>=1,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers, changeRecommendedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
assertRequirement(
"label:Code-Review<1,users=human_reviewers",
ImmutableList.of(changeNoVotesByReviewers),
ImmutableList.of(
changeApprovedByAllReviewers,
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers));
assertRequirement(
"label:Code-Review>1,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// match changes where all reviewers have any (non-zero) vote
assertRequirement(
"label:Code-Review=ANY,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers, changeRecommendedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// votes of the change owners are ignored (as the change owner is not considered as a reviewer)
addReviews(project, changeApprovedByAllReviewers, ReviewInput.dislike(), owner);
assertRequirement(
"label:Code-Review=MAX,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// missing votes from service users are fine
addReviewers(project, changeApprovedByAllReviewers, serviceUser);
assertRequirement(
"label:Code-Review=MAX,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// votes from service users are ignored
addReviews(project, changeApprovedByAllReviewers, ReviewInput.dislike(), serviceUser);
assertRequirement(
"label:Code-Review=MAX,users=human_reviewers",
ImmutableList.of(changeApprovedByAllReviewers),
ImmutableList.of(
changeApprovedBySomeReviewers,
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
// when reviewers by email are present changes do not match, unless the expected value is 0
projectOperations.project(project).forUpdate().enableReviewerByEmail().update();
Change.Id changeRecommendedByAllReviewersWithReviewersByEmail =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(
project,
changeRecommendedByAllReviewersWithReviewersByEmail,
reviewer1,
reviewer2,
reviewer3);
addReviews(
project,
changeRecommendedByAllReviewersWithReviewersByEmail,
ReviewInput.recommend(),
reviewer1,
reviewer2,
reviewer3);
addReviewer(
project,
changeRecommendedByAllReviewersWithReviewersByEmail,
"email-without-account@example.com");
Change.Id changeNoVotesByReviewersWithReviewersByEmail =
changeOperations.newChange().project(project).owner(owner).createV1();
addReviewers(
project, changeNoVotesByReviewersWithReviewersByEmail, reviewer1, reviewer2, reviewer3);
addReviewer(
project, changeNoVotesByReviewersWithReviewersByEmail, "email-without-account@example.com");
assertRequirement(
"label:Code-Review=MAX,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review=2,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review=ANY,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review=0,users=human_reviewers",
ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review<=2,users=human_reviewers",
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of());
assertRequirement(
"label:Code-Review<=0,users=human_reviewers",
ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review<=-1,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review<2,users=human_reviewers",
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of());
assertRequirement(
"label:Code-Review<1,users=human_reviewers",
ImmutableList.of(changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of(changeRecommendedByAllReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review<0,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review>=0,users=human_reviewers",
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of());
assertRequirement(
"label:Code-Review>=1,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
assertRequirement(
"label:Code-Review>-1,users=human_reviewers",
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail),
ImmutableList.of());
assertRequirement(
"label:Code-Review>0,users=human_reviewers",
ImmutableList.of(),
ImmutableList.of(
changeRecommendedByAllReviewersWithReviewersByEmail,
changeNoVotesByReviewersWithReviewersByEmail));
// cannot combine users=human_reviewers" with submit record status
assertError(
"label:Code-Review=ok,users=human_reviewers",
changeApprovedByAllReviewers,
"Cannot use the 'users=human_reviewers' argument in conjunction with a submit record label"
+ " status");
// cannot combine "users" arg with a "user" arg
assertError(
"label:Code-Review=MAX,users=human_reviewers,user=reviewer1",
changeApprovedByAllReviewers,
"Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+ " group')");
// cannot combine "users" arg with a "group" arg
assertError(
"label:Code-Review=MAX,users=human_reviewers,group=foo",
changeApprovedByAllReviewers,
"Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+ " group')");
// cannot combine "users" arg with a positional arg
assertError(
"label:Code-Review=MAX,users=human_reviewers,reviewer1",
changeApprovedByAllReviewers,
"Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+ " group')");
assertError(
"label:Code-Review=MAX,reviewer1,users=human_reviewers",
changeApprovedByAllReviewers,
"Cannot use the 'users' argument in conjunction with other arguments ('count', 'user',"
+ " group')");
// label without "users=human_reviewers" still works
assertRequirement(
"label:Code-Review=MAX,user=reviewer1",
ImmutableList.of(changeApprovedByAllReviewers, changeApprovedBySomeReviewers),
ImmutableList.of(
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
assertRequirement(
"label:Code-Review=MAX,reviewer1",
ImmutableList.of(changeApprovedByAllReviewers, changeApprovedBySomeReviewers),
ImmutableList.of(
changeRecommendedByAllReviewers,
changeRecommendedBySomeReviewers,
changeNoVotesByReviewers));
}
@Test
public void hasUnresolvedComments() throws Exception {
TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
PushOneCommit.Result r =
pushFactory
.create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
.to("refs/for/master");
assertNotMatching("has:unresolved", r.getChange().getId());
assertMatching("-has:unresolved", r.getChange().getId());
ReviewInput reviewInput = new ReviewInput();
ReviewInput.CommentInput commentInput = new ReviewInput.CommentInput();
commentInput.line = 1;
commentInput.message = "inline";
commentInput.unresolved = true;
reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(commentInput));
gApi.changes().id(project.get(), r.getChange().getId().get()).current().review(reviewInput);
CommentInfo commentInfo =
Iterables.getOnlyElement(
gApi.changes()
.id(project.get(), r.getChange().getId().get())
.current()
.commentsAsList());
assertMatching("has:unresolved", r.getChange().getId());
assertNotMatching("-has:unresolved", r.getChange().getId());
// Rename the file in the change to check that the change still matches if the commented file
// path is no longer present in the current patch set.
gApi.changes().id(project.get(), r.getChange().getId().get()).edit().create();
gApi.changes()
.id(project.get(), r.getChange().getId().get())
.edit()
.renameFile("file.txt", "renamed-file.txt");
gApi.changes()
.id(project.get(), r.getChange().getId().get())
.edit()
.publish(new PublishChangeEditInput());
Map<String, FileInfo> files =
gApi.changes().id(project.get(), r.getChange().getId().get()).current().files("1");
assertThat(files.keySet()).containsExactly(Patch.COMMIT_MSG, "renamed-file.txt");
assertThat(files.get("renamed-file.txt").oldPath).isEqualTo("file.txt");
assertMatching("has:unresolved", r.getChange().getId());
assertNotMatching("-has:unresolved", r.getChange().getId());
// Resolve the comment.
reviewInput = new ReviewInput();
commentInput = new ReviewInput.CommentInput();
commentInput.inReplyTo = commentInfo.id;
commentInput.message = "done";
commentInput.unresolved = false;
reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(commentInput));
gApi.changes().id(project.get(), r.getChange().getId().get()).current().review(reviewInput);
assertNotMatching("has:unresolved", r.getChange().getId());
assertMatching("-has:unresolved", r.getChange().getId());
}
private void addReviewers(Project.NameKey project, Change.Id changeId, Account.Id... reviewers)
throws Exception {
for (Account.Id reviewer : reviewers) {
addReviewer(project, changeId, reviewer.toString());
}
}
private void addReviewer(Project.NameKey project, Change.Id changeId, String reviewer)
throws Exception {
gApi.changes().id(project.get(), changeId.get()).addReviewer(reviewer);
}
private void addReviews(
Project.NameKey project, Change.Id changeId, ReviewInput reviewInput, Account.Id... reviewers)
throws Exception {
for (Account.Id reviewer : reviewers) {
requestScopeOperations.setApiUser(reviewer);
gApi.changes().id(project.get(), changeId.get()).current().review(reviewInput);
}
}
private void approveAsUser(String changeId, Account.Id userId) throws Exception {
requestScopeOperations.setApiUser(userId);
approve(changeId);
}
private static void assertUploader(ChangeInfo changeInfo, String email) {
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).uploader.email)
.isEqualTo(email);
}
private static void assertCommitter(ChangeInfo changeInfo, String email) {
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.committer.email)
.isEqualTo(email);
}
private static void assertAuthor(ChangeInfo changeInfo, String email) {
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.author.email)
.isEqualTo(email);
}
private void allowLabelPermission(
String labelName, String refPattern, AccountGroup.UUID group, int minVote, int maxVote) {
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(labelName).ref(refPattern).group(group).range(minVote, maxVote))
.update();
}
private PushOneCommit.Result createGitSubmoduleCommit(String ref) throws Exception {
return pushFactory
.create(admin.newIdent(), testRepo, "subject", ImmutableMap.of())
.addGitSubmodule(
"modules/module-a", ObjectId.fromString("19f1787342cb15d7e82a762f6b494e91ccb4dd34"))
.to(ref);
}
private PushOneCommit.Result createNormalCommit(
PersonIdent personIdent, String ref, String fileName) throws Exception {
return pushFactory
.create(personIdent, testRepo, "subject", ImmutableMap.of(fileName, fileName))
.to(ref);
}
private PushOneCommit.Result createNormalCommit(String ref, String fileName) throws Exception {
return pushFactory
.create(admin.newIdent(), testRepo, "subject", ImmutableMap.of(fileName, fileName))
.to(ref);
}
private PushOneCommit.Result createMergeCommitChange(
String ref, RevCommit parent1, RevCommit parent2, @Nullable ObjectId treeId)
throws Exception {
PushOneCommit m =
pushFactory
.create(admin.newIdent(), testRepo)
.setParents(ImmutableList.of(parent1, parent2));
if (treeId != null) {
m.setTopLevelTreeId(treeId);
}
PushOneCommit.Result result = m.to(ref);
result.assertOkStatus();
return result;
}
private ObjectId mergeAndGetTreeId(RevCommit c1, RevCommit c2) throws Exception {
ThreeWayMerger threeWayMerger = MergeStrategy.RESOLVE.newMerger(repo(), true);
threeWayMerger.setBase(c1.getParent(0));
boolean mergeResult = threeWayMerger.merge(c1, c2);
assertThat(mergeResult).isTrue();
return threeWayMerger.getResultTreeId();
}
private void assertRequirement(
String requirement,
ImmutableList<Change.Id> matchingChanges,
ImmutableList<Change.Id> nonMatchingChanges) {
for (Change.Id matchingChange : matchingChanges) {
assertMatching(requirement, matchingChange);
}
for (Change.Id nonMatchingChange : nonMatchingChanges) {
assertNotMatching(requirement, nonMatchingChange);
}
}
private void assertMatching(String requirement, Change.Id change) {
assertWithMessage("requirement \"%s\" doesn't match change %s", requirement, change)
.that(evaluate(requirement, change).status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
}
private void assertNotMatching(String requirement, Change.Id change) {
assertWithMessage("requirement \"%s\" matches change %s", requirement, change)
.that(evaluate(requirement, change).status())
.isEqualTo(SubmitRequirementExpressionResult.Status.FAIL);
}
private void assertError(String requirement, Change.Id change, String errorMessage) {
SubmitRequirementExpressionResult result = evaluate(requirement, change);
assertThat(result.status()).isEqualTo(SubmitRequirementExpressionResult.Status.ERROR);
assertThat(result.errorMessage().get()).isEqualTo(errorMessage);
}
private SubmitRequirementExpressionResult evaluate(String requirement, Change.Id change) {
ChangeData cd = changeDataFactory.create(project, change);
return submitRequirementsEvaluator.evaluateExpression(
SubmitRequirementExpression.create(requirement), cd);
}
}