blob: 89c3a77942e6d43c7bf6d210064ced4d2deabd9e [file]
// Copyright (C) 2026 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.server.project;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.server.restapi.project.MigrateLabelFunctionsToSubmitRequirement;
import com.google.gerrit.server.restapi.project.MigrateLabelFunctionsToSubmitRequirement.Status;
import com.google.gerrit.server.schema.UpdateUI;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import com.google.gerrit.testing.TestUpdateUI;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
/** Tests full migration behavior of {@link MigrateLabelFunctionsToSubmitRequirement}. */
public class MigrateLabelFunctionsToSubmitRequirementTest {
private static final String LABEL_NAME = "Foo";
private static final ImmutableList<LabelValue> STANDARD_VALUES =
ImmutableList.of(
LabelValue.create((short) -1, "Looks Bad"),
LabelValue.create((short) 0, "No Score"),
LabelValue.create((short) 1, "Looks Good"));
private InMemoryRepositoryManager repoManager;
private MigrateLabelFunctionsToSubmitRequirement migrator;
@Before
public void setUp() {
repoManager = new InMemoryRepositoryManager();
migrator = new MigrateLabelFunctionsToSubmitRequirement(null, repoManager);
}
private record TestProjectConfig(
ProjectConfig config,
Map<String, LabelType> labels,
Map<String, SubmitRequirement> submitRequirements) {}
private TestProjectConfig newConfig() {
ProjectConfig config = mock(ProjectConfig.class);
Map<String, LabelType> labels = new LinkedHashMap<>();
Map<String, SubmitRequirement> submitRequirements = new LinkedHashMap<>();
when(config.getLabelSections()).thenReturn(labels);
when(config.getSubmitRequirementSections()).thenReturn(submitRequirements);
return new TestProjectConfig(config, labels, submitRequirements);
}
private LabelType.Builder labelBuilder(LabelFunction function) {
return LabelType.builder(LABEL_NAME, STANDARD_VALUES).setFunction(function);
}
private void createRepository(Project.NameKey project) throws Exception {
var repo = repoManager.createRepository(project);
assertThat(repo).isNotNull();
}
private SubmitRequirement requirementFor(ProjectConfig config) {
Map<String, SubmitRequirement> srs = config.getSubmitRequirementSections();
assertThat(srs).containsKey(LABEL_NAME);
return srs.get(LABEL_NAME);
}
@Test
public void maxWithBlock_createsSr_andResetsLabelFunction() throws Exception {
Project.NameKey project = Project.nameKey("p-max");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels().put(LABEL_NAME, labelBuilder(LabelFunction.MAX_WITH_BLOCK).build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(requirementFor(c.config()).submittabilityExpression().expressionString())
.isEqualTo("label:Foo=MAX AND -label:Foo=MIN");
assertThat(c.labels().get(LABEL_NAME).getFunction()).isEqualTo(LabelFunction.NO_BLOCK);
}
@Test
public void noBlock_doesNotCreateSr_andReturnsNoChange() throws Exception {
Project.NameKey project = Project.nameKey("p-noblock");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels().put(LABEL_NAME, labelBuilder(LabelFunction.NO_BLOCK).build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.NO_CHANGE);
assertThat(c.submitRequirements()).isEmpty();
verifyNoInteractions(ui);
}
@Test
public void noOp_resetsToNoBlock_withoutSr() throws Exception {
Project.NameKey project = Project.nameKey("p-noop");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels().put(LABEL_NAME, labelBuilder(LabelFunction.NO_OP).build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(c.submitRequirements()).isEmpty();
assertThat(c.labels().get(LABEL_NAME).getFunction()).isEqualTo(LabelFunction.NO_BLOCK);
}
@Test
public void existingSrWithSameName_isNotOverwritten_andWarningEmitted() throws Exception {
Project.NameKey project = Project.nameKey("p-existing");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels().put(LABEL_NAME, labelBuilder(LabelFunction.MAX_WITH_BLOCK).build());
c.submitRequirements()
.put(
LABEL_NAME,
SubmitRequirement.builder()
.setName(LABEL_NAME)
.setSubmittabilityExpression(SubmitRequirementExpression.create("project:foo"))
.setAllowOverrideInChildProjects(false)
.build());
TestUpdateUI ui = new TestUpdateUI();
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(c.labels().get(LABEL_NAME).getFunction()).isEqualTo(LabelFunction.NO_BLOCK);
assertThat(c.submitRequirements().get(LABEL_NAME).submittabilityExpression().expressionString())
.isEqualTo("project:foo");
assertThat(ui.getOutput()).contains("Warning");
}
@Test
public void branchPattern_regex_usedAsIs() throws Exception {
Project.NameKey project = Project.nameKey("p-regex");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("^refs/heads/main-.*"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:^refs/heads/main-.*");
}
@Test
public void branchPattern_wildcard_convertedToRegex() throws Exception {
Project.NameKey project = Project.nameKey("p-wildcard");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("refs/heads/release/*"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:^\\Qrefs/heads/release/\\E.*");
}
@Test
public void branchPattern_plain_wrappedInQuotes() throws Exception {
Project.NameKey project = Project.nameKey("p-plain");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("refs/heads/master"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:\"refs/heads/master\"");
}
@Test
public void branchPattern_plain_withQuote_isEscapedAndQuoted() throws Exception {
Project.NameKey project = Project.nameKey("p-quote");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("refs/heads/gerr\"it"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:\"refs/heads/gerr\\\"it\"");
}
@Test
public void branchPattern_plain_withHash_isQuoted() throws Exception {
Project.NameKey project = Project.nameKey("p-hash");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("refs/heads/gerr#it"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:\"refs/heads/gerr#it\"");
}
@Test
public void branchPattern_multiple_joinedWithOr() throws Exception {
Project.NameKey project = Project.nameKey("p-multi");
createRepository(project);
TestProjectConfig c = newConfig();
c.labels()
.put(
LABEL_NAME,
labelBuilder(LabelFunction.MAX_WITH_BLOCK)
.setRefPatterns(ImmutableList.of("refs/heads/master", "^refs/heads/main-.*"))
.build());
UpdateUI ui = mock(UpdateUI.class);
Status status = migrator.updateConfig(project, c.config(), ui);
assertThat(status).isEqualTo(Status.MIGRATED);
assertThat(
requirementFor(c.config()).applicabilityExpression().orElseThrow().expressionString())
.isEqualTo("branch:\"refs/heads/master\" OR branch:^refs/heads/main-.*");
}
}