| // 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.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; |
| import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static com.google.gerrit.server.project.testing.TestLabels.label; |
| import static com.google.gerrit.server.project.testing.TestLabels.value; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.MoreCollectors; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.ExtensionRegistry; |
| import com.google.gerrit.acceptance.ExtensionRegistry.Registration; |
| import com.google.gerrit.acceptance.NoHttpd; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestAccount; |
| import com.google.gerrit.acceptance.UseTimezone; |
| import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes; |
| import com.google.gerrit.acceptance.testsuite.change.IndexOperations; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.LabelFunction; |
| import com.google.gerrit.entities.LabelId; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.LegacySubmitRequirement; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.SubmitRecord; |
| import com.google.gerrit.entities.SubmitRequirement; |
| import com.google.gerrit.entities.SubmitRequirementExpression; |
| import com.google.gerrit.entities.SubmitRequirementExpressionResult; |
| import com.google.gerrit.entities.SubmitRequirementResult; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput; |
| import com.google.gerrit.extensions.api.changes.RevisionApi; |
| import com.google.gerrit.extensions.api.groups.GroupInput; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.SubmitType; |
| import com.google.gerrit.extensions.common.ActionInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.LabelDefinitionInput; |
| import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo; |
| import com.google.gerrit.extensions.common.SubmitRecordInfo; |
| import com.google.gerrit.extensions.common.SubmitRequirementExpressionInfo; |
| import com.google.gerrit.extensions.common.SubmitRequirementInput; |
| import com.google.gerrit.extensions.common.SubmitRequirementResultInfo; |
| import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.httpd.raw.IndexPreloadingUtil; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import com.google.gerrit.server.project.testing.TestLabels; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.rules.SubmitRule; |
| import com.google.inject.Inject; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.stream.IntStream; |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.transport.RefSpec; |
| import org.eclipse.jgit.util.RawParseUtils; |
| import org.junit.Test; |
| |
| @NoHttpd |
| @UseTimezone(timezone = "US/Eastern") |
| @VerifyNoPiiInChangeNotes(true) |
| public class SubmitRequirementIT extends AbstractDaemonTest { |
| @Inject private ProjectOperations projectOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private ExtensionRegistry extensionRegistry; |
| @Inject private IndexOperations.Change changeIndexOperations; |
| |
| @Test |
| public void submitRecords() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| TestSubmitRule testSubmitRule = new TestSubmitRule(); |
| try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) { |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRecords).hasSize(2); |
| // Check the default submit record for the code-review label |
| SubmitRecordInfo codeReviewRecord = Iterables.get(change.submitRecords, 0); |
| assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule"); |
| assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.NOT_READY); |
| assertThat(codeReviewRecord.labels).hasSize(1); |
| SubmitRecordInfo.Label label = Iterables.getOnlyElement(codeReviewRecord.labels); |
| assertThat(label.label).isEqualTo("Code-Review"); |
| assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.NEED); |
| assertThat(label.appliedBy).isNull(); |
| // Check the custom test record created by the TestSubmitRule |
| SubmitRecordInfo testRecord = Iterables.get(change.submitRecords, 1); |
| assertThat(testRecord.ruleName).isEqualTo("testSubmitRule"); |
| assertThat(testRecord.status).isEqualTo(SubmitRecordInfo.Status.OK); |
| assertThat(testRecord.requirements) |
| .containsExactly(new LegacySubmitRequirementInfo("OK", "fallback text", "type")); |
| assertThat(testRecord.labels).hasSize(1); |
| SubmitRecordInfo.Label testLabel = Iterables.getOnlyElement(testRecord.labels); |
| assertThat(testLabel.label).isEqualTo("label"); |
| assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK); |
| assertThat(testLabel.appliedBy).isNull(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| // Code review record is satisfied after voting +2 |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRecords).hasSize(2); |
| codeReviewRecord = Iterables.get(change.submitRecords, 0); |
| assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule"); |
| assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.OK); |
| assertThat(codeReviewRecord.labels).hasSize(1); |
| label = Iterables.getOnlyElement(codeReviewRecord.labels); |
| assertThat(label.label).isEqualTo("Code-Review"); |
| assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.OK); |
| assertThat(label.appliedBy._accountId).isEqualTo(admin.id().get()); |
| } |
| } |
| |
| @Test |
| public void checkSubmitRequirement_fromRefsConfigChange_satisfied() throws Exception { |
| String oldHead = projectOperations.project(project).getHead("master").name(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) |
| .update(); |
| fetchRefsMetaConfig(); |
| PushOneCommit.Result configResult = |
| createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2"); |
| |
| // Upload a normal change. Check the SR against it. |
| testRepo.reset(oldHead); |
| PushOneCommit.Result r2 = createChange(); |
| String changeId = r2.getChangeId(); |
| SubmitRequirementResultInfo info = |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(configResult.getChange().getId().toString()) |
| .get(); |
| assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED); |
| voteLabel(changeId, "Code-Review", 2); |
| info = |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(configResult.getChange().getId().toString()) |
| .get(); |
| assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_fromRefsConfigChangeOfAnotherProject_satisfied() |
| throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) |
| .update(); |
| fetchRefsMetaConfig(); |
| PushOneCommit.Result configResult = |
| createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2"); |
| |
| // Upload a normal change in another project. Check the SR against it. |
| Project.NameKey otherProject = projectOperations.newProject().create(); |
| TestRepository<InMemoryRepository> otherRepo = cloneProject(otherProject, admin); |
| PushOneCommit.Result r2 = createChange(otherRepo); |
| String changeId = r2.getChangeId(); |
| SubmitRequirementResultInfo info = |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(configResult.getChange().getId().toString()) |
| .get(); |
| assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED); |
| voteLabel(changeId, "Code-Review", 2); |
| info = |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(configResult.getChange().getId().toString()) |
| .get(); |
| assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_fromRefsConfigChange_failsForNonExistingSR() throws Exception { |
| String oldHead = projectOperations.project(project).getHead("master").name(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) |
| .update(); |
| fetchRefsMetaConfig(); |
| PushOneCommit.Result configResult = |
| createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2"); |
| |
| // Upload a normal change. Check the SR against it. |
| testRepo.reset(oldHead); |
| PushOneCommit.Result r2 = createChange(); |
| String changeId = r2.getChangeId(); |
| Exception thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Bar") |
| .refsConfigChangeId(configResult.getChange().getId().toString()) |
| .get()); |
| assertThat(thrown).hasMessageThat().isEqualTo("No submit requirement matching name 'Bar'"); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_notAllowedFromNonRefsConfigChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| Exception thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(r.getChange().getId().toString()) |
| .get()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "Change '%s' is not in refs/meta/config branch.", r.getChange().getId().get())); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_notAllowedFromNonExistingChange() throws Exception { |
| String invalidChangeNumber = "2134789"; |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| Exception thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .srName("Foo") |
| .refsConfigChangeId(invalidChangeNumber) |
| .get()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo(String.format("Change '%s' does not exist", invalidChangeNumber)); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_fromRefsConfigChange_failsIfBothParametersAreNotSet() |
| throws Exception { |
| String oldHead = projectOperations.project(project).getHead("master").name(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS)) |
| .update(); |
| fetchRefsMetaConfig(); |
| PushOneCommit.Result configResult = |
| createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2"); |
| |
| // Upload a normal change. Check the SR against it. |
| testRepo.reset(oldHead); |
| PushOneCommit.Result r2 = createChange(); |
| String changeId = r2.getChangeId(); |
| Exception thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(changeId).checkSubmitRequirementRequest().srName("Bar").get()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo("Both 'sr-name' and 'refs-config-change-id' parameters must be set"); |
| |
| thrown = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| gApi.changes() |
| .id(changeId) |
| .checkSubmitRequirementRequest() |
| .refsConfigChangeId(configResult.getChangeId()) |
| .get()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo("Both 'sr-name' and 'refs-config-change-id' parameters must be set"); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_satisfied() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| SubmitRequirementInput in = |
| createSubmitRequirementInput( |
| "Code-Review", /* submittabilityExpression= */ "label:Code-Review=+2"); |
| |
| SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_notApplicable() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| SubmitRequirementInput in = |
| createSubmitRequirementInput( |
| "Code-Review", |
| /* applicableIf= */ "branch:non-existent", |
| /* submittableIf= */ "label:Code-Review=+2", |
| /* overrideIf= */ null); |
| |
| SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_overridden() throws Exception { |
| configLabel("Override-Label", LabelFunction.NO_OP); // label function has no effect |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Override-Label") |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| SubmitRequirementInput in = |
| createSubmitRequirementInput( |
| "Code-Review", |
| /* applicableIf= */ null, |
| /* submittableIf= */ "label:Code-Review=+2", |
| /* overrideIf= */ "label:Override-Label=+1"); |
| |
| SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED); |
| |
| voteLabel(changeId, "Override-Label", 1); |
| result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.OVERRIDDEN); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_error() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| SubmitRequirementInput in = |
| createSubmitRequirementInput("Code-Review", /* submittabilityExpression= */ "!!!"); |
| |
| SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.ERROR); |
| } |
| |
| @Test |
| public void checkSubmitRequirement_verifiesUploader() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| voteLabel(changeId, "Code-Review", 2); |
| TestAccount anotherUser = accountCreator.createValid("anotherUser"); |
| |
| SubmitRequirementInput in = |
| createSubmitRequirementInput( |
| "Foo", /* submittabilityExpression= */ "uploader:" + anotherUser.id()); |
| SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED); |
| |
| in = |
| createSubmitRequirementInput( |
| "Foo", /* submittabilityExpression= */ "uploader:" + r.getChange().change().getOwner()); |
| result = gApi.changes().id(changeId).checkSubmitRequirement(in); |
| assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED); |
| } |
| |
| @Test |
| public void submitRequirement_withLabelEqualsMax() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=MAX")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_withLabelEqualsMax_fromNonUploader() throws Exception { |
| configLabel("my-label", LabelFunction.NO_OP); // label function has no effect |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1)) |
| .update(); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("my-label") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:my-label=MAX,user=non_uploader")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| // The second requirement is coming from the legacy code-review label function |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Voting with a max vote as the uploader will not satisfy the submit requirement. |
| voteLabel(changeId, "my-label", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Voting as a non-uploader will satisfy the submit requirement. |
| requestScopeOperations.setApiUser(user.id()); |
| voteLabel(changeId, "my-label", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_withLabelEqualsMinBlockingSubmission() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("-label:Code-Review=MIN")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Requirement is satisfied because there are no votes |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement (coming from the label function definition) is not satisfied. We return |
| // both legacy and non-legacy requirements in this case since their statuses are not identical. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| voteLabel(changeId, "Code-Review", -1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Requirement is still satisfied because -1 is not the max negative value |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| voteLabel(changeId, "Code-Review", -2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // Requirement is now unsatisfied because -2 is the max negative value |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_withMaxWithBlock_ignoringSelfApproval() throws Exception { |
| configLabel("my-label", LabelFunction.MAX_WITH_BLOCK); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1)) |
| .update(); |
| |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("my-label") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| "label:my-label=MAX,user=non_uploader -label:my-label=MIN")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // Create the change as admin |
| requestScopeOperations.setApiUser(admin.id()); |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Admin (a.k.a uploader) adds a -1 min vote. This is going to block submission. |
| voteLabel(changeId, "my-label", -1); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| // The other requirement is coming from the code-review label function |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // user (i.e. non_uploader) votes 1. Requirement is still blocking because of -1 of uploader. |
| requestScopeOperations.setApiUser(user.id()); |
| voteLabel(changeId, "my-label", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Admin (a.k.a uploader) removes -1. Now requirement is fulfilled. |
| requestScopeOperations.setApiUser(admin.id()); |
| voteLabel(changeId, "my-label", 0); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_withLabelEqualsAny() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=ANY")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Legacy and non-legacy requirements have mismatching status. Both are returned from the API. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsFulfilled() |
| throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Verified") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirementIsNotApplicable_whenApplicabilityExpressionIsNotFulfilled() |
| throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setApplicabilityExpression(SubmitRequirementExpression.of("project:foo")) |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirementIsOverridden_whenOverrideExpressionIsFulfilled() throws Exception { |
| configLabel("build-cop-override", LabelFunction.NO_BLOCK); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("build-cop-override") |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "build-cop-override", 1); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_overriddenInChildProjectWithStricterRequirement() throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Override submit requirement in child project (requires Code-Review=+2 instead of +1) |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_overriddenInChildProjectWithLessStrictRequirement() |
| throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Override submit requirement in child project (requires Code-Review=+1 instead of +2) |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement: override in child project applies |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_overriddenInChildProjectAsDisabled() throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Custom-Requirement") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Override submit requirement in child project (requires Code-Review=+1 instead of +2) |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Custom-Requirement") |
| .setApplicabilityExpression(SubmitRequirementExpression.of("is:false")) |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "Custom-Requirement", |
| Status.NOT_APPLICABLE, |
| /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_inheritedFromParentProject() throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_overriddenSRInParentProjectIsInheritedByChildProject() |
| throws Exception { |
| // Define submit requirement in root project. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Override submit requirement in parent project (requires Code-Review=+2 instead of +1). |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| Project.NameKey child = createProjectOverAPI("child", project, true, /* submitType= */ null); |
| TestRepository<InMemoryRepository> childRepo = cloneProject(child); |
| PushOneCommit.Result r = createChange(childRepo); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_ignoredInChildProject_ifParentDoesNotAllowOverride() |
| throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // Override submit requirement in child project (requires Code-Review=+2 instead of +1). |
| // Will have no effect since parent does not allow override. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement: override in child project was ignored |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_ignoredInChildProject_ifParentAddsSRThatDoesNotAllowOverride() |
| throws Exception { |
| // Submit requirement in child project (requires Code-Review=+1) |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| // Add stricter non-overridable submit requirement in parent project (requires Code-Review=+2, |
| // instead of Code-Review=+1) |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_ignoredInChildProject_ifParentMakesSRNonOverridable() |
| throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Override submit requirement in child project (requires Code-Review=+1 instead of +2) |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement: override in child project applies |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| // Disallow overriding the submit requirement in the parent project. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_ignoredInGrandChildProject_ifGrandParentDoesNotAllowOverride() |
| throws Exception { |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| Project.NameKey grandChild = |
| createProjectOverAPI("grandChild", project, true, /* submitType= */ null); |
| |
| // Override submit requirement in grand child project (requires Code-Review=+2 instead of +1). |
| // Will have no effect since grand parent does not allow override. |
| configSubmitRequirement( |
| grandChild, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| TestRepository<InMemoryRepository> grandChildRepo = cloneProject(grandChild); |
| PushOneCommit.Result r = createChange(grandChildRepo); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement: override in grand child project was ignored |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_overrideOverideExpression() throws Exception { |
| // Define submit requirement in root project. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Create Code-Review-Override label |
| LabelDefinitionInput input = new LabelDefinitionInput(); |
| input.function = "NoOp"; |
| input.values = ImmutableMap.of("+1", "Override", " 0", "No Override"); |
| gApi.projects().name(project.get()).label("Code-Review-Override").create(input); |
| |
| // Allow to vote on the Code-Review-Override label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("Code-Review-Override") |
| .range(0, 1) |
| .ref("refs/*") |
| .group(REGISTERED_USERS) |
| .build()) |
| .update(); |
| |
| // Override submit requirement in project (requires Code-Review-Override+1 as override instead |
| // of build-cop-override+1). |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:Code-Review-Override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review-Override", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Code-Review-Override+1 was enough to fulfill the override expression of the requirement |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirementThatOverridesParentSubmitRequirementTakesEffectImmediately() |
| throws Exception { |
| // Define submit requirement in root project that ignores self approvals from the uploader. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=MAX,user=non_uploader")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Apply a self approval from the uploader. |
| voteLabel(changeId, "Code-Review", 2); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Code-Review+2 is ignored since it's a self approval from the uploader |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Already satisfied since it |
| // doesn't ignore self approvals. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| |
| // since the change is not submittable we expect the submit action to be not returned |
| assertThat(gApi.changes().id(changeId).current().actions()).doesNotContainKey("submit"); |
| |
| // Override submit requirement in project (allow uploaders to self approve). |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=MAX")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // the self approval from the uploader is no longer ignored, hence the submit requirement is |
| // satisfied now |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| // since the change is submittable now we expect the submit action to be returned |
| Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions(); |
| assertThat(actions).containsKey("submit"); |
| ActionInfo submitAction = actions.get("submit"); |
| assertThat(submitAction.enabled).isTrue(); |
| } |
| |
| @Test |
| public void |
| submitRequirementThatOverridesParentSubmitRequirementTakesEffectImmediately_staleIndex() |
| throws Exception { |
| // Define submit requirement in root project that ignores self approvals from the uploader. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=MAX,user=non_uploader")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Apply a self approval from the uploader. |
| voteLabel(changeId, "Code-Review", 2); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // Code-Review+2 is ignored since it's a self approval from the uploader |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Already satisfied since it |
| // doesn't ignore self approvals. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| |
| // since the change is not submittable we expect the submit action to be not returned |
| assertThat(gApi.changes().id(changeId).current().actions()).doesNotContainKey("submit"); |
| |
| // disable change index writes so that the change in the index gets stale when the new submit |
| // requirement is added |
| try (AutoCloseable ignored = changeIndexOperations.disableWrites()) { |
| // Override submit requirement in project (allow uploaders to self approve). |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:Code-Review=MAX")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // the self approval from the uploader is no longer ignored, hence the submit requirement is |
| // satisfied now |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| // since the change is submittable now we expect the submit action to be returned |
| Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions(); |
| assertThat(actions).containsKey("submit"); |
| ActionInfo submitAction = actions.get("submit"); |
| assertThat(submitAction.enabled).isTrue(); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_partiallyOverriddenSRIsIgnored() throws Exception { |
| // Create build-cop-override label |
| LabelDefinitionInput input = new LabelDefinitionInput(); |
| input.function = "NoOp"; |
| input.values = ImmutableMap.of("+1", "Override", " 0", "No Override"); |
| gApi.projects().name(project.get()).label("build-cop-override").create(input); |
| |
| // Allow to vote on the build-cop-override label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("build-cop-override") |
| .range(0, 1) |
| .ref("refs/*") |
| .group(REGISTERED_USERS) |
| .build()) |
| .update(); |
| |
| // Define submit requirement in root project. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Create Code-Review-Override label |
| gApi.projects().name(project.get()).label("Code-Review-Override").create(input); |
| |
| // Allow to vote on the Code-Review-Override label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| TestProjectUpdate.allowLabel("Code-Review-Override") |
| .range(0, 1) |
| .ref("refs/*") |
| .group(REGISTERED_USERS) |
| .build()) |
| .update(); |
| |
| // Override submit requirement in project (requires Code-Review-Override+1 as override instead |
| // of build-cop-override+1), but do not set all required properties (submittability expression |
| // is missing). We update the project.config file directly in the remote repository, since |
| // trying to push such a submit requirement would be rejected by the commit validation. |
| projectOperations |
| .project(project) |
| .forInvalidation() |
| .addProjectConfigUpdater( |
| config -> |
| config.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| "Code-Review", |
| ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION, |
| "label:Code-Review-Override=+1")) |
| .invalidate(); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review-Override", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // The override expression in the project is satisfied, but it's ignored since the SR is |
| // incomplete. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "build-cop-override", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // The submit requirement is overridden now (the override expression in the child project is |
| // ignored) |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_storedForClosedChanges() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| ChangeNotes notes = notesFactory.create(project, r.getChange().getId()); |
| |
| SubmitRequirementResult result = |
| notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement()); |
| assertSubmitRequirementResult( |
| result, |
| "Code-Review", |
| SubmitRequirementResult.Status.SATISFIED, |
| /* submitExpr= */ "label:Code-Review=MAX", |
| SubmitRequirementExpressionResult.Status.PASS); |
| |
| // Adding comments does not affect the stored SRs. |
| addComment(r.getChangeId(), /* file= */ "foo"); |
| notes = notesFactory.create(project, r.getChange().getId()); |
| result = notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement()); |
| assertSubmitRequirementResult( |
| result, |
| "Code-Review", |
| SubmitRequirementResult.Status.SATISFIED, |
| /* submitExpr= */ "label:Code-Review=MAX", |
| SubmitRequirementExpressionResult.Status.PASS); |
| assertThat(notes.getHumanComments()).hasSize(1); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_storedForAbandonedChanges() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| gApi.changes().id(r.getChangeId()).abandon(); |
| ChangeNotes notes = notesFactory.create(project, r.getChange().getId()); |
| SubmitRequirementResult result = |
| notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement()); |
| assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED); |
| assertThat(result.submittabilityExpressionResult().get().status()) |
| .isEqualTo(SubmitRequirementExpressionResult.Status.PASS); |
| assertThat(result.submittabilityExpressionResult().get().expression().expressionString()) |
| .isEqualTo("label:Code-Review=MAX"); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_loadedFromTheLatestRevisionNoteForClosedChanges() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| // Abandon change. Submit requirements get stored in the revision note of patch-set 1. |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Restore the change. |
| gApi.changes().id(changeId).restore(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Upload a second patch-set, fulfill the CR submit requirement. |
| amendChange(changeId, "refs/for/master", user, repo); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.revisions).hasSize(2); |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| // Abandon the change. |
| gApi.changes().id(changeId).abandon(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_abandonRestoreUpdateMerge() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| // Abandon change. Submit requirements get stored in the revision note of patch-set 1. |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Restore the change. |
| gApi.changes().id(changeId).restore(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Update the change. |
| amendChange(changeId, "refs/for/master", user, repo); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.revisions).hasSize(2); |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| // Merge the change. |
| gApi.changes().id(changeId).current().submit(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_returnsEmpty_forAbandonedChangeWithPreviouslyStoredSRs() |
| throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| // Abandon change. Submit requirements get stored in the revision note of patch-set 1. |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Clear SRs for the project and update code-review label to be non-blocking. |
| clearSubmitRequirements(project); |
| @SuppressWarnings("deprecation") |
| LabelType cr = |
| TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build(); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(cr); |
| u.save(); |
| } |
| |
| // Restore the change. No SRs apply. |
| gApi.changes().id(changeId).restore(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).isEmpty(); |
| |
| // Abandon the change. Still, no SRs apply. |
| gApi.changes().id(changeId).abandon(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).isEmpty(); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_returnsEmpty_forMergedChangeWithPreviouslyStoredSRs() |
| throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| // Abandon change. Submit requirements get stored in the revision note of patch-set 1. |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Clear SRs for the project and update code-review label to be non-blocking. |
| clearSubmitRequirements(project); |
| @SuppressWarnings("deprecation") |
| LabelType cr = |
| TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build(); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(cr); |
| u.save(); |
| } |
| |
| // Restore the change. No SRs apply. |
| gApi.changes().id(changeId).restore(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).isEmpty(); |
| |
| // Merge the change. Still, no SRs apply. |
| gApi.changes().id(changeId).current().submit(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).isEmpty(); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_withMultipleAbandonAndRestore() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| |
| // Abandon change. Submit requirements get stored in the revision note of patch-set 1. |
| gApi.changes().id(changeId).abandon(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Restore the change. |
| gApi.changes().id(changeId).restore(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Abandon the change again. |
| gApi.changes().id(changeId).abandon(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Restore, vote CR=+2, and abandon again. Make sure the requirement is now satisfied. |
| gApi.changes().id(changeId).restore(); |
| voteLabel(changeId, "Code-Review", 2); |
| gApi.changes().id(changeId).abandon(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_retrievedFromNoteDbForAbandonedChanges() throws Exception { |
| for (SubmitType submitType : SubmitType.values()) { |
| Project.NameKey project = createProjectForPush(submitType); |
| TestRepository<InMemoryRepository> repo = cloneProject(project); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = |
| createChange(repo, "master", "Add a file", "foo", "content", "topic"); |
| String changeId = r.getChangeId(); |
| voteLabel(changeId, "Code-Review", 2); |
| gApi.changes().id(changeId).abandon(); |
| |
| // Add another submit requirement. This will not get returned for the abandoned change, since |
| // we return the state of the SR results when the change was abandoned. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("New-Requirement") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("-has:unresolved")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| ChangeInfo changeInfo = |
| gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS); |
| assertThat(changeInfo.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:Code-Review=MAX"); |
| |
| // Restore the change, the new requirement will show up |
| gApi.changes().id(changeId).restore(); |
| changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS); |
| assertThat(changeInfo.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:Code-Review=MAX"); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, |
| "New-Requirement", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "-has:unresolved"); |
| |
| // Abandon again, make sure the new requirement was persisted |
| gApi.changes().id(changeId).abandon(); |
| changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS); |
| assertThat(changeInfo.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:Code-Review=MAX"); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, |
| "New-Requirement", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "-has:unresolved"); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_retrievedFromNoteDbForClosedChanges() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| |
| gApi.changes().id(changeId).current().submit(); |
| |
| // Add new submit requirement |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Verified") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // The new "Verified" submit requirement is not returned, since this change is closed |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void |
| submitRequirements_returnOneEntryForMatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled() |
| throws Exception { |
| // Configure a legacy submit requirement: label with a max with block function |
| configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("build-cop-override") |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Configure a submit requirement with the same name. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("build-cop-override") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| "label:build-cop-override=MAX -label:build-cop-override=MIN")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // Create a change. Vote to fulfill all requirements. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| voteLabel(changeId, "build-cop-override", 1); |
| voteLabel(changeId, "Code-Review", 2); |
| |
| // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco. |
| // Only non-legacy bco is returned. |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "build-cop-override", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN"); |
| assertThat(change.submittable).isTrue(); |
| |
| // Merge the change. Submit requirements are still the same. |
| gApi.changes().id(changeId).current().submit(); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "build-cop-override", |
| Status.SATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN"); |
| } |
| |
| @Test |
| public void |
| submitRequirements_returnTwoEntriesForMismatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled() |
| throws Exception { |
| // Configure a legacy submit requirement: label with a max with block function |
| configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("build-cop-override") |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Configure a submit requirement with the same name. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("build-cop-override") |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create("label:build-cop-override=MIN")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // Create a change |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| voteLabel(changeId, "build-cop-override", 1); |
| voteLabel(changeId, "Code-Review", 2); |
| |
| // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco. |
| // Two instances of bco will be returned since their status is not matching. |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(3); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "build-cop-override", |
| Status.SATISFIED, |
| /* isLegacy= */ true, |
| // MAX_WITH_BLOCK function was translated to a submittability expression. |
| /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN"); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "build-cop-override", |
| Status.UNSATISFIED, |
| /* isLegacy= */ false, |
| /* submittabilityCondition= */ "label:build-cop-override=MIN"); |
| assertThat(change.submittable).isFalse(); |
| } |
| |
| @Test |
| public void submitRequirements_skippedIfLegacySRIsBasedOnOptionalLabel() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| SubmitRule r1 = |
| createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY); |
| try (Registration registration = extensionRegistry.newRegistration().add(r1)) { |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements; |
| assertThat(submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_notSkippedIfLegacySRIsBasedOnNonOptionalLabel() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| SubmitRule r1 = |
| createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK); |
| try (Registration registration = extensionRegistry.newRegistration().add(r1)) { |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements; |
| assertThat(submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true); |
| } |
| } |
| |
| @Test |
| public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception { |
| configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("build-cop-override") |
| .ref("refs/heads/master") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // 1. Project has two legacy requirements: Code-Review and bco. Both unsatisfied. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "build-cop-override", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| // 2. Vote +1 on bco. bco becomes satisfied |
| voteLabel(changeId, "build-cop-override", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true); |
| |
| // 3. Vote +1 on Code-Review. Code-Review becomes satisfied |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true); |
| |
| // 4. Merge the change. Submit requirements status is presented from NoteDb. |
| gApi.changes().id(changeId).current().submit(); |
| change = gApi.changes().id(changeId).get(); |
| // Legacy submit records are returned as submit requirements. |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_backFilledFromIndexForActiveChanges() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| // Query the change. ChangeInfo is back-filled from the change index. |
| List<ChangeInfo> changeInfos = |
| gApi.changes() |
| .query() |
| .withQuery("project:{" + project.get() + "} (status:open OR status:closed)") |
| .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS)) |
| .get(); |
| assertThat(changeInfos).hasSize(1); |
| assertSubmitRequirementStatus( |
| changeInfos.get(0).submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_backFilledFromIndexForClosedChanges() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| gApi.changes().id(changeId).current().submit(); |
| |
| // Query the change. ChangeInfo is back-filled from the change index. |
| List<ChangeInfo> changeInfos = |
| gApi.changes() |
| .query() |
| .withQuery("project:{" + project.get() + "} (status:open OR status:closed)") |
| .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS)) |
| .get(); |
| assertThat(changeInfos).hasSize(1); |
| assertSubmitRequirementStatus( |
| changeInfos.get(0).submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_applicabilityExpressionIsAlwaysHidden() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setApplicabilityExpression(SubmitRequirementExpression.of("branch:refs/heads/master")) |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| ChangeInfo changeInfo = gApi.changes().id(changeId).get(); |
| SubmitRequirementResultInfo requirement = |
| changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement()); |
| assertSubmitRequirementExpression( |
| requirement.applicabilityExpressionResult, |
| /* expression= */ null, |
| /* passingAtoms= */ null, |
| /* failingAtoms= */ null, |
| /* status= */ SubmitRequirementExpressionInfo.Status.PASS, |
| /* fulfilled= */ true); |
| assertThat(requirement.submittabilityExpressionResult).isNotNull(); |
| } |
| |
| @Test |
| public void submitRequirement_nonApplicable_submittabilityAndOverrideNotEvaluated() |
| throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setApplicabilityExpression( |
| SubmitRequirementExpression.of("branch:refs/heads/non-existent")) |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get())) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| ChangeInfo changeInfo = gApi.changes().id(changeId).get(); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false); |
| SubmitRequirementResultInfo requirement = |
| changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement()); |
| assertSubmitRequirementExpression( |
| requirement.applicabilityExpressionResult, |
| /* expression= */ null, |
| /* passingAtoms= */ null, |
| /* failingAtoms= */ null, |
| /* status= */ SubmitRequirementExpressionInfo.Status.FAIL, |
| /* fulfilled= */ false); |
| assertThat(requirement.submittabilityExpressionResult.status) |
| .isEqualTo(SubmitRequirementExpressionInfo.Status.NOT_EVALUATED); |
| assertThat(requirement.submittabilityExpressionResult.expression) |
| .isEqualTo(SubmitRequirementExpression.maxCodeReview().expressionString()); |
| assertThat(requirement.overrideExpressionResult.status) |
| .isEqualTo(SubmitRequirementExpressionInfo.Status.NOT_EVALUATED); |
| assertThat(requirement.overrideExpressionResult.expression) |
| .isEqualTo("project:" + project.get()); |
| } |
| |
| @Test |
| public void submitRequirement_emptyApplicable_submittabilityAndOverrideEvaluated() |
| throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setApplicabilityExpression(Optional.empty()) |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("project:non-existent")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| |
| ChangeInfo changeInfo = gApi.changes().id(changeId).get(); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| SubmitRequirementResultInfo requirement = |
| changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement()); |
| assertThat(requirement.applicabilityExpressionResult).isNull(); |
| assertSubmitRequirementExpression( |
| requirement.submittabilityExpressionResult, |
| /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(), |
| /* passingAtoms= */ ImmutableList.of( |
| SubmitRequirementExpression.maxCodeReview().expressionString()), |
| /* failingAtoms= */ ImmutableList.of(), |
| /* status= */ SubmitRequirementExpressionInfo.Status.PASS, |
| /* fulfilled= */ true); |
| assertSubmitRequirementExpression( |
| requirement.overrideExpressionResult, |
| /* expression= */ "project:non-existent", |
| /* passingAtoms= */ ImmutableList.of(), |
| /* failingAtoms= */ ImmutableList.of("project:non-existent"), |
| /* status= */ SubmitRequirementExpressionInfo.Status.FAIL, |
| /* fulfilled= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_overriden_submittabilityEvaluated() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setApplicabilityExpression(Optional.empty()) |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get())) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| |
| ChangeInfo changeInfo = gApi.changes().id(changeId).get(); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false); |
| SubmitRequirementResultInfo requirement = |
| changeInfo.submitRequirements.stream() |
| .filter(sr -> !sr.isLegacy) |
| .collect(MoreCollectors.onlyElement()); |
| assertThat(requirement.applicabilityExpressionResult).isNull(); |
| assertSubmitRequirementExpression( |
| requirement.submittabilityExpressionResult, |
| /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(), |
| /* passingAtoms= */ ImmutableList.of(), |
| /* failingAtoms= */ ImmutableList.of( |
| SubmitRequirementExpression.maxCodeReview().expressionString()), |
| /* status= */ SubmitRequirementExpressionInfo.Status.FAIL, |
| /* fulfilled= */ false); |
| assertSubmitRequirementExpression( |
| requirement.overrideExpressionResult, |
| /* expression= */ "project:" + project.get(), |
| /* passingAtoms= */ ImmutableList.of("project:" + project.get()), |
| /* failingAtoms= */ ImmutableList.of(), |
| /* status= */ SubmitRequirementExpressionInfo.Status.PASS, |
| /* fulfilled= */ true); |
| } |
| |
| @Test |
| public void submitRequirements_eliminatesDuplicatesForLegacyNonMatchingSRs() throws Exception { |
| // If a custom/prolog submit rule emits the same label name multiple times, we merge these into |
| // a single submit requirement result: in this test, we have two different submit rules that |
| // return the same label name, one as "OK" and the other as "NEED". The submit requirements |
| // API favours the blocking entry and returns one SR result with status=UNSATISFIED. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| SubmitRule r1 = |
| createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK); |
| SubmitRule r2 = |
| createSubmitRule("r2", SubmitRecord.Status.NOT_READY, "CR", SubmitRecord.Label.Status.NEED); |
| try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) { |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements; |
| assertThat(submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| submitRequirements, "CR", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| } |
| |
| @Test |
| public void submitRequirements_eliminatesDuplicatesForLegacyMatchingSRs() throws Exception { |
| // If a custom/prolog submit rule emits the same label name multiple times, we merge these into |
| // a single submit requirement result: in this test, we have two different submit rules that |
| // return the same label name, but both are fulfilled (i.e. they both allow submission). The |
| // submit requirements API returns one SR result with status=SATISFIED. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| SubmitRule r1 = |
| createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK); |
| SubmitRule r2 = |
| createSubmitRule("r2", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY); |
| try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) { |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements; |
| assertThat(submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true); |
| } |
| } |
| |
| @Test |
| public void submitRequirements_eliminatesMultipleDuplicatesForLegacyMatchingSRs() |
| throws Exception { |
| // If a custom/prolog submit rule emits the same label name multiple times, we merge these into |
| // a single submit requirement result: in this test, we have five different submit rules that |
| // return the same label name, all with an "OK" status. The submit requirements API returns |
| // a single SR result with status=SATISFIED. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| try (Registration registration = extensionRegistry.newRegistration()) { |
| IntStream.range(0, 5) |
| .forEach( |
| i -> |
| registration.add( |
| createSubmitRule( |
| "r" + i, SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK))); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements; |
| assertThat(submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true); |
| } |
| } |
| |
| @Test |
| public void submitRequirement_duplicateSubmitRequirement_sameCase() throws Exception { |
| // Define 2 submit requirements with exact same name but different submittability expression. |
| try (TestRepository<Repository> repo = |
| new TestRepository<>(repoManager.openRepository(project))) { |
| Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG); |
| RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId()); |
| RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG); |
| byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE); |
| String projectConfig = RawParseUtils.decode(data); |
| |
| repo.update( |
| RefNames.REFS_CONFIG, |
| repo.commit() |
| .parent(head) |
| .message("Set project config") |
| .add( |
| ProjectConfig.PROJECT_CONFIG, |
| projectConfig |
| // JGit parses this as a list value: |
| // submit-requirement.Code-Review.submittableIf = |
| // [label:Code-Review=+2, label:Code-Review=+1] |
| // if getString is used to read submittableIf JGit returns the last value |
| // (label:Code-Review=+1) |
| + "[submit-requirement \"Code-Review\"]\n" |
| + " submittableIf = label:Code-Review=+2\n" |
| + "[submit-requirement \"Code-Review\"]\n" |
| + " submittableIf = label:Code-Review=+1\n")); |
| } |
| projectCache.evict(project); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // The submit requirement is fulfilled now, since label:Code-Review=+1 applies as submittability |
| // expression (see comment above) |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_duplicateSubmitRequirement_differentCase() throws Exception { |
| // Define 2 submit requirements with same name but different case and different submittability |
| // expression. |
| try (TestRepository<Repository> repo = |
| new TestRepository<>(repoManager.openRepository(project))) { |
| Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG); |
| RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId()); |
| RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG); |
| byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE); |
| String projectConfig = RawParseUtils.decode(data); |
| |
| repo.update( |
| RefNames.REFS_CONFIG, |
| repo.commit() |
| .parent(head) |
| .message("Set project config") |
| .add( |
| ProjectConfig.PROJECT_CONFIG, |
| projectConfig |
| // ProjectConfig processes the submit requirements in the order in which they |
| // appear (1. Code-Review, 2. code-review) and ignores any further submit |
| // requirement if its name case-insensitively matches the name of a submit |
| // requirement that has already been seen. This means the Code-Review submit |
| // requirement applies and the code-review submit requirement is ignored. |
| + "[submit-requirement \"Code-Review\"]\n" |
| + " submittableIf = label:Code-Review=+2\n" |
| + "[submit-requirement \"code-review\"]\n" |
| + " submittableIf = label:Code-Review=+1\n")); |
| } |
| projectCache.evict(project); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // Still not satisfied since the Code-Review submit requirement with label:Code-Review=+2 as |
| // submittability expression applies (see comment above). |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // The submit requirement is fulfilled now, since label:Code-Review=+2 applies as submittability |
| // expression (see comment above) |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void submitRequirement_overrideInheritedSRWithDifferentNameCasing() throws Exception { |
| // Define submit requirement in root project and allow override. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| // Define a submit requirement with the same name in the child project that differs by case and |
| // has a different submittability expression (requires Code-Review=+1 instead of +2). |
| // This overrides the inherited submit requirement with the same name, although the case is |
| // different. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("code-review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| // +1 was enough to fulfill the requirement since the override applies |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false); |
| // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_cannotOverrideNonOverridableInheritedSRWithDifferentNameCasing() |
| throws Exception { |
| // Define submit requirement in root project and disallow override. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| // Define a submit requirement with the same name in the child project that differs by case and |
| // has a different submittability expression (requires Code-Review=+1 instead of +2). |
| // This is ignored since the inherited submit requirement with the same name (different case) |
| // disallows overriding. |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("code-review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1")) |
| .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 1); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // Still not satisfied since the override is ignored. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| |
| @Test |
| public void globalSubmitRequirement_storedForClosedChanges() throws Exception { |
| SubmitRequirement globalSubmitRequirement = |
| SubmitRequirement.builder() |
| .setName("global-submit-requirement") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test")) |
| .setAllowOverrideInChildProjects(false) |
| .build(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(globalSubmitRequirement)) { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "global-submit-requirement", |
| Status.UNSATISFIED, |
| /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| |
| voteLabel(changeId, "Code-Review", 2); |
| gApi.changes().id(changeId).topic("test"); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, |
| "global-submit-requirement", |
| Status.SATISFIED, |
| /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| |
| gApi.changes().id(changeId).current().submit(); |
| |
| ChangeNotes notes = notesFactory.create(project, r.getChange().getId()); |
| SubmitRequirementResult result = |
| notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement()); |
| assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED); |
| assertThat(result.submittabilityExpressionResult().get().status()) |
| .isEqualTo(SubmitRequirementExpressionResult.Status.PASS); |
| assertThat(result.submittabilityExpressionResult().get().expression().expressionString()) |
| .isEqualTo("topic:test"); |
| } |
| } |
| |
| @Test |
| public void projectSubmitRequirementDuplicatesGlobal_overrideNotAllowed_globalEvaluated() |
| throws Exception { |
| SubmitRequirement globalSubmitRequirement = |
| SubmitRequirement.builder() |
| .setName("CoDe-reView") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test")) |
| .setAllowOverrideInChildProjects(false) |
| .build(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(globalSubmitRequirement)) { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Vote does not satisfy submit requirement, because the global definition is evaluated. |
| voteLabel(changeId, "CoDe-reView", 2); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false); |
| // In addition, the legacy submit requirement is emitted, since the status mismatch |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| |
| // Setting the topic satisfies the global definition. |
| gApi.changes().id(changeId).topic("test"); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void projectSubmitRequirementDuplicatesGlobal_overrideAllowed_projectRequirementEvaluated() |
| throws Exception { |
| SubmitRequirement globalSubmitRequirement = |
| SubmitRequirement.builder() |
| .setName("CoDe-reView") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test")) |
| .setAllowOverrideInChildProjects(true) |
| .build(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(globalSubmitRequirement)) { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Setting the topic does not satisfy submit requirement, because the project definition is |
| // evaluated. |
| gApi.changes().id(changeId).topic("test"); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| // There is no mismatch with legacy submit requirement, so the single result is emitted. |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Voting satisfies the project definition. |
| voteLabel(changeId, "Code-Review", 2); |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void legacySubmitRequirementDuplicatesGlobal_statusMatches_globalReturned() |
| throws Exception { |
| // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement. |
| testLegacySubmitRequirementDuplicatesGlobalStatusMatches(/*allowOverrideInChildProject=*/ true); |
| testLegacySubmitRequirementDuplicatesGlobalStatusMatches( |
| /*allowOverrideInChildProject=*/ false); |
| } |
| |
| private void testLegacySubmitRequirementDuplicatesGlobalStatusMatches( |
| boolean allowOverrideInChildProject) throws Exception { |
| SubmitRequirement globalSubmitRequirement = |
| SubmitRequirement.builder() |
| .setName("CoDe-reView") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test")) |
| .setAllowOverrideInChildProjects(allowOverrideInChildProject) |
| .build(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(globalSubmitRequirement)) { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Both are evaluated, but only the global is returned, since both are unsatisfied |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Both are evaluated, but only the global is returned, since both are satisfied |
| voteLabel(changeId, "Code-Review", 2); |
| gApi.changes().id(changeId).topic("test"); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void legacySubmitRequirementWithIgnoreSelfApproval() throws Exception { |
| LabelType verified = |
| label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| verified = verified.toBuilder().setIgnoreSelfApproval(true).build(); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(verified); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(verified.getName()) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // The DefaultSubmitRule emits an "OK" submit record for Verified, while the |
| // ignoreSelfApprovalRule emits a "NEED" submit record. The "submit requirements" adapter merges |
| // both results and returns the blocking one only. |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes().id(changeId).addReviewer(user.id().toString()); |
| |
| voteLabel(changeId, verified.getName(), +1); |
| ChangeInfo changeInfo = gApi.changes().id(changeId).get(); |
| Collection<SubmitRequirementResultInfo> submitRequirements = changeInfo.submitRequirements; |
| assertSubmitRequirementStatus( |
| submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void legacySubmitRequirementDuplicatesGlobal_statusDoesNotMatch_bothRecordsReturned() |
| throws Exception { |
| // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement. |
| testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch( |
| /*allowOverrideInChildProject=*/ true); |
| testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch( |
| /*allowOverrideInChildProject=*/ false); |
| } |
| |
| private void testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch( |
| boolean allowOverrideInChildProject) throws Exception { |
| SubmitRequirement globalSubmitRequirement = |
| SubmitRequirement.builder() |
| .setName("CoDe-reView") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test")) |
| .setAllowOverrideInChildProjects(allowOverrideInChildProject) |
| .build(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(globalSubmitRequirement)) { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| // Both are evaluated, but only the global is returned, since both are unsatisfied |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| // Both are evaluated and both are returned, since result mismatch |
| voteLabel(changeId, "Code-Review", 2); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| gApi.changes().id(changeId).topic("test"); |
| gApi.changes().id(changeId).reviewer(admin.id().toString()).deleteVote(LabelId.CODE_REVIEW); |
| |
| change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false); |
| } |
| } |
| |
| @Test |
| public void submitRequirements_disallowsTheIsSubmittableOperator() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Wrong-Req") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:submittable")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| SubmitRequirementResultInfo srResult = |
| change.submitRequirements.stream() |
| .filter(sr -> sr.name.equals("Wrong-Req")) |
| .collect(MoreCollectors.onlyElement()); |
| assertThat(srResult.status).isEqualTo(Status.ERROR); |
| assertThat(srResult.submittabilityExpressionResult.errorMessage) |
| .isEqualTo("Operator 'is:submittable' cannot be used in submit requirement expressions."); |
| } |
| |
| @Test |
| public void submitRequirements_forcedByDirectSubmission() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(REGISTERED_USERS)) |
| .update(); |
| |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("My-Requirement") |
| // Submit requirement is always unsatisfied, but we are going to bypass it. |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master%submit"); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "My-Requirement", Status.FORCED, /* isLegacy= */ false); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.FORCED, /* isLegacy= */ true); |
| } |
| |
| @Test |
| public void submitRequirement_evaluatedWithInternalUserCredentials() throws Exception { |
| GroupInput in = new GroupInput(); |
| in.name = "invisible-group"; |
| in.visibleToAll = false; |
| in.ownerId = adminGroupUuid().get(); |
| gApi.groups().create(in); |
| |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("My-Requirement") |
| .setApplicabilityExpression(SubmitRequirementExpression.of("ownerin:invisible-group")) |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| SubmitRequirementResultInfo srResult = |
| change.submitRequirements.stream() |
| .filter(sr -> sr.name.equals("My-Requirement")) |
| .collect(MoreCollectors.onlyElement()); |
| assertThat(srResult.status).isEqualTo(Status.NOT_APPLICABLE); |
| } |
| |
| @Test |
| public void submitRequirements_submittedTogetherWithoutLegacySubmitRequirements() |
| throws Exception { |
| // Add a code review submit requirement and mark the 'Code-Review' label function to be |
| // non-blocking. |
| configSubmitRequirement( |
| allProjects, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview()) |
| .setAllowOverrideInChildProjects(true) |
| .build()); |
| |
| @SuppressWarnings("deprecation") |
| LabelType cr = TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build(); |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().upsertLabelType(cr); |
| u.save(); |
| } |
| |
| // Create two changes in a chain. |
| createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Make sure the CR requirement is unsatisfied. |
| String changeId = r2.getChangeId(); |
| ChangeInfo change = gApi.changes().id(changeId).get(); |
| assertThat(change.submitRequirements).hasSize(1); |
| assertSubmitRequirementStatus( |
| change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false); |
| |
| List<ChangeInfo> changeInfos = gApi.changes().id(changeId).submittedTogether(); |
| assertThat(changeInfos).hasSize(2); |
| assertThat( |
| changeInfos.stream() |
| .map(c -> c.submittable) |
| .distinct() |
| .collect(MoreCollectors.onlyElement())) |
| .isFalse(); |
| } |
| |
| @Test |
| public void queryChangesBySubmitRequirementResultUsingTheLabelPredicate() throws Exception { |
| // Create a non-blocking label and a submit-requirement that necessitates voting on this label. |
| configLabel("LC", LabelFunction.NO_OP); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allowLabel("LC").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1)) |
| .update(); |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("LC") |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("label:LC=MAX")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| |
| List<ChangeInfo> changeInfos = gApi.changes().query("label:LC=NEED").get(); |
| assertThat(changeInfos).hasSize(1); |
| assertThat(changeInfos.get(0).changeId).isEqualTo(changeId); |
| assertThat(gApi.changes().query("label:LC=OK").get()).isEmpty(); |
| // case does not matter |
| changeInfos = gApi.changes().query("label:lc=NEED").get(); |
| assertThat(changeInfos).hasSize(1); |
| assertThat(changeInfos.get(0).changeId).isEqualTo(changeId); |
| |
| voteLabel(r.getChangeId(), "LC", +1); |
| changeInfos = gApi.changes().query("label:LC=OK").get(); |
| assertThat(changeInfos.get(0).changeId).isEqualTo(changeId); |
| assertThat(gApi.changes().query("label:LC=NEED").get()).isEmpty(); |
| } |
| |
| @Test |
| public void queryingChangesWithSubmitRequirementOptionDoesNotTouchDatabase() throws Exception { |
| configSubmitRequirement( |
| project, |
| SubmitRequirement.builder() |
| .setName("Code-Review") |
| // Always not submittable |
| .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false")) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| PushOneCommit.Result r1 = createChange(); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get(); |
| assertThat(changeInfo.submitRequirements).hasSize(2); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy = */ false); |
| assertSubmitRequirementStatus( |
| changeInfo.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy = */ true); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| try (AutoCloseable ignored = disableNoteDb()) { |
| List<ChangeInfo> changeInfos = |
| gApi.changes() |
| .query() |
| .withQuery("project:{" + project.get() + "} (status:open OR status:closed)") |
| .withOptions( |
| new ImmutableSet.Builder<ListChangesOption>() |
| .addAll(IndexPreloadingUtil.DASHBOARD_OPTIONS) |
| .add(ListChangesOption.SUBMIT_REQUIREMENTS) |
| .build()) |
| .get(); |
| assertThat(changeInfos).hasSize(1); |
| assertSubmitRequirementStatus( |
| changeInfos.get(0).submitRequirements, |
| "Code-Review", |
| Status.UNSATISFIED, |
| /* isLegacy = */ false); |
| assertSubmitRequirementStatus( |
| changeInfos.get(0).submitRequirements, |
| "Code-Review", |
| Status.SATISFIED, |
| /* isLegacy = */ true); |
| } |
| } |
| |
| private void voteLabel(String changeId, String labelName, int score) throws RestApiException { |
| gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score)); |
| } |
| |
| private void assertSubmitRequirementResult( |
| SubmitRequirementResult result, |
| String srName, |
| SubmitRequirementResult.Status status, |
| String submitExpr, |
| SubmitRequirementExpressionResult.Status submitStatus) { |
| assertThat(result.submitRequirement().name()).isEqualTo(srName); |
| assertThat(result.status()).isEqualTo(status); |
| assertThat(result.submittabilityExpressionResult().get().expression().expressionString()) |
| .isEqualTo(submitExpr); |
| assertThat(result.submittabilityExpressionResult().get().status()).isEqualTo(submitStatus); |
| } |
| |
| private void assertSubmitRequirementStatus( |
| Collection<SubmitRequirementResultInfo> results, |
| String requirementName, |
| SubmitRequirementResultInfo.Status status, |
| boolean isLegacy, |
| String submittabilityCondition) { |
| for (SubmitRequirementResultInfo result : results) { |
| if (result.name.equals(requirementName) |
| && result.status == status |
| && result.isLegacy == isLegacy |
| && result.submittabilityExpressionResult.expression.equals(submittabilityCondition)) { |
| return; |
| } |
| } |
| throw new AssertionError( |
| String.format( |
| "Could not find submit requirement %s with status %s (results = %s)", |
| requirementName, |
| status, |
| results.stream() |
| .map(r -> String.format("%s=%s", r.name, r.status)) |
| .collect(toImmutableList()))); |
| } |
| |
| private void assertSubmitRequirementStatus( |
| Collection<SubmitRequirementResultInfo> results, |
| String requirementName, |
| SubmitRequirementResultInfo.Status status, |
| boolean isLegacy) { |
| for (SubmitRequirementResultInfo result : results) { |
| if (result.name.equals(requirementName) |
| && result.status == status |
| && result.isLegacy == isLegacy) { |
| return; |
| } |
| } |
| throw new AssertionError( |
| String.format( |
| "Could not find submit requirement %s with status %s (results = %s)", |
| requirementName, |
| status, |
| results.stream() |
| .map(r -> String.format("%s=%s, legacy=%s", r.name, r.status, r.isLegacy)) |
| .collect(toImmutableList()))); |
| } |
| |
| private void assertSubmitRequirementExpression( |
| SubmitRequirementExpressionInfo result, |
| @Nullable String expression, |
| @Nullable List<String> passingAtoms, |
| @Nullable List<String> failingAtoms, |
| SubmitRequirementExpressionInfo.Status status, |
| boolean fulfilled) { |
| assertThat(result.expression).isEqualTo(expression); |
| if (passingAtoms == null) { |
| assertThat(result.passingAtoms).isNull(); |
| } else { |
| assertThat(result.passingAtoms).containsExactlyElementsIn(passingAtoms); |
| } |
| if (failingAtoms == null) { |
| assertThat(result.failingAtoms).isNull(); |
| } else { |
| assertThat(result.failingAtoms).containsExactlyElementsIn(failingAtoms); |
| } |
| assertThat(result.status).isEqualTo(status); |
| assertThat(result.fulfilled).isEqualTo(fulfilled); |
| } |
| |
| private Project.NameKey createProjectForPush(SubmitType submitType) throws Exception { |
| Project.NameKey project = projectOperations.newProject().submitType(submitType).create(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid())) |
| .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid())) |
| .update(); |
| return project; |
| } |
| |
| private static SubmitRule createSubmitRule( |
| String ruleName, |
| SubmitRecord.Status srStatus, |
| String labelName, |
| SubmitRecord.Label.Status labelStatus) { |
| return changeData -> { |
| SubmitRecord r = new SubmitRecord(); |
| r.ruleName = ruleName; |
| r.status = srStatus; |
| SubmitRecord.Label label = new SubmitRecord.Label(); |
| label.label = labelName; |
| label.status = labelStatus; |
| r.labels = Arrays.asList(label); |
| return Optional.of(r); |
| }; |
| } |
| |
| /** Returns a hard-coded submit record containing all fields. */ |
| private static class TestSubmitRule implements SubmitRule { |
| @Override |
| public Optional<SubmitRecord> evaluate(ChangeData changeData) { |
| SubmitRecord record = new SubmitRecord(); |
| record.ruleName = "testSubmitRule"; |
| record.status = SubmitRecord.Status.OK; |
| SubmitRecord.Label label = new SubmitRecord.Label(); |
| label.label = "label"; |
| label.status = SubmitRecord.Label.Status.OK; |
| record.labels = Arrays.asList(label); |
| record.requirements = |
| Arrays.asList( |
| LegacySubmitRequirement.builder() |
| .setType("type") |
| .setFallbackText("fallback text") |
| .build()); |
| return Optional.of(record); |
| } |
| } |
| |
| private static SubmitRequirementInput createSubmitRequirementInput( |
| String name, String submittabilityExpression) { |
| SubmitRequirementInput input = new SubmitRequirementInput(); |
| input.name = name; |
| input.submittabilityExpression = submittabilityExpression; |
| return input; |
| } |
| |
| private static SubmitRequirementInput createSubmitRequirementInput( |
| String name, String applicableIf, String submittableIf, String overrideIf) { |
| SubmitRequirementInput input = new SubmitRequirementInput(); |
| input.name = name; |
| input.applicabilityExpression = applicableIf; |
| input.submittabilityExpression = submittableIf; |
| input.overrideExpression = overrideIf; |
| return input; |
| } |
| |
| private void addComment(String changeId, String file) throws Exception { |
| ReviewInput in = new ReviewInput(); |
| CommentInput ci = new CommentInput(); |
| ci.path = file; |
| ci.message = "message"; |
| ci.line = 1; |
| in.comments = ImmutableMap.of("foo", ImmutableList.of(ci)); |
| gApi.changes().id(changeId).current().review(in); |
| } |
| |
| private void fetchRefsMetaConfig() throws Exception { |
| git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call(); |
| testRepo.reset(RefNames.REFS_CONFIG); |
| } |
| |
| private PushOneCommit.Result createConfigChangeWithSubmitRequirement( |
| String srName, String submitExpression) throws Exception { |
| Config cfg = projectOperations.project(project).getConfig(); |
| cfg.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| srName, |
| ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| submitExpression); |
| return createConfigChange(cfg); |
| } |
| |
| private PushOneCommit.Result createConfigChange(Config cfg) throws Exception { |
| PushOneCommit.Result r = |
| pushFactory |
| .create( |
| admin.newIdent(), testRepo, "Update project config", "project.config", cfg.toText()) |
| .to("refs/for/refs/meta/config"); |
| r.assertOkStatus(); |
| return r; |
| } |
| } |