blob: 86434896d3b29d546ee4218d80400c4f92a222f1 [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.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.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.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.RefNames;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
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 distinctVoters_sameUserVotesOnDifferentLabels_fails() throws Exception {
Change.Id c1 = changeOperations.newChange().project(project).create();
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).create();
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).create();
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).create();
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).create();
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).create();
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).create();
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).create();
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).create(),
"failed to parse the parent number xyz: For input string: \"xyz\"");
assertError(
"has:submodule-update,base=1,arg=foo",
changeOperations.newChange().project(project).create(),
"wrong number of arguments for the has:submodule-update operator");
assertError(
"has:submodule-update,base",
changeOperations.newChange().project(project).create(),
"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().author(authorId).committer(committerId).create();
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().author(authorId).committer(committerId).create();
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());
}
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 assertMatching(String requirement, Change.Id change) {
assertThat(evaluate(requirement, change).status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
}
private void assertNotMatching(String requirement, Change.Id change) {
assertThat(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);
}
}