blob: 56e23a487ce9cc88b0dd4fe586a761bf1f4302f1 [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.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.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.Change;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
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 SubmitRequirementsEvaluator 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 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");
}
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(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);
}
}