Split owners integration tests into a separate build targets

So that they can be executed in parallel boosting the overal testing
performance (especially on RBE).

Change-Id: I3c291517c6080abe50fe7a4735913b652de340f1
diff --git a/owners/BUILD b/owners/BUILD
index 402bd34..f08ec84 100644
--- a/owners/BUILD
+++ b/owners/BUILD
@@ -12,8 +12,8 @@
     name = "gerrit-owners-predicates",
     srcs = PROLOG_PREDICATES,
     deps = [
+        "//owners-common",
         "@prolog-runtime//jar:neverlink",
-        "//owners-common:owners-common",
     ] + PLUGIN_DEPS_NEVERLINK,
 )
 
@@ -50,15 +50,37 @@
 java_library(
     name = "owners__plugin_test_deps",
     testonly = 1,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = [
+            "src/test/java/**/*Test.java",
+            "src/test/java/**/*IT.java",
+        ],
+    ),
     visibility = ["//visibility:public"],
     exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
         ":owners",
     ],
+    deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":owners",
+    ],
 )
 
 junit_tests(
     name = "owners_tests",
-    srcs = glob(["src/test/java/**/*.java"]),
+    srcs = glob(["src/test/java/**/*Test.java"]),
     tags = ["owners"],
-    deps = ["owners__plugin_test_deps"],
+    deps = [
+        ":owners__plugin_test_deps",
+    ],
 )
+
+[junit_tests(
+    name = f[:f.index(".")].replace("/", "_"),
+    srcs = [f],
+    tags = ["owners"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":owners__plugin_test_deps",
+    ],
+) for f in glob(["src/test/java/**/*IT.java"])]
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
index 667227c..9fdb4b9 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersApprovalHasOperandIT.java
@@ -21,6 +21,8 @@
 
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GlobalPluginConfig;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
@@ -38,7 +40,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
-public class OwnersApprovalHasOperandIT extends OwnersSubmitRequirementIT {
+@TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
+@UseLocalDisk
+public class OwnersApprovalHasOperandIT extends OwnersSubmitRequirementITAbstract {
   private static final String REQUIREMENT_NAME = "Owner-Approval";
 
   // This configuration is needed on 3.5 only and should be removed during/after the merge to
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
index 5f6be28..0d5c2c5 100644
--- a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementIT.java
@@ -15,505 +15,9 @@
 
 package com.googlesource.gerrit.owners;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
-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.labelBuilder;
-import static com.google.gerrit.server.project.testing.TestLabels.value;
-import static java.util.stream.Collectors.joining;
-
-import com.google.gerrit.acceptance.GitUtil;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
-import com.google.gerrit.acceptance.config.GlobalPluginConfig;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
-import com.google.gerrit.entities.LabelFunction;
-import com.google.gerrit.entities.LabelId;
-import com.google.gerrit.entities.LabelType;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.changes.ChangeApi;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
-import com.google.gerrit.extensions.common.SubmitRecordInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.project.testing.TestLabels;
-import com.google.inject.Inject;
-import com.googlesource.gerrit.owners.common.LabelDefinition;
-import java.util.Collection;
-import java.util.stream.Stream;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.junit.Test;
 
 @TestPlugin(name = "owners", sysModule = "com.googlesource.gerrit.owners.OwnersModule")
 @UseLocalDisk
-public class OwnersSubmitRequirementIT extends LightweightPluginDaemonTest {
-  private static final LegacySubmitRequirementInfo NOT_READY =
-      new LegacySubmitRequirementInfo("NOT_READY", "Owners", "owners");
-  private static final LegacySubmitRequirementInfo READY =
-      new LegacySubmitRequirementInfo("OK", "Owners", "owners");
-
-  @Inject protected RequestScopeOperations requestScopeOperations;
-  @Inject private ProjectOperations projectOperations;
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireAtLeastOneApprovalForMatchingPathFromOwner() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    TestAccount user1 = accountCreator.user1();
-    addOwnerFileWithMatchersToRoot(true, ".md", admin2, user1);
-
-    PushOneCommit.Result r = createChange("Add a file", "README.md", "foo");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
-    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
-    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldNotRequireApprovalForNotMatchingPath() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileWithMatchersToRoot(true, ".md", admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "README.txt", "foo");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).isEmpty();
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeReady = changeApi.get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).isEmpty();
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireApprovalFromRootOwner() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
-    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
-    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldBlockOwnersApprovalForMaxNegativeVote() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-
-    changeApi.current().review(ReviewInput.reject());
-    assertThat(forChange(r).get().submittable).isFalse();
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireVerifiedApprovalEvenIfCodeOwnerApproved() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, admin2);
-
-    installVerifiedLabel();
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    assertThat(changeApi.get().submittable).isFalse();
-    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.approve());
-    assertThat(forChange(r).get().submittable).isFalse();
-    assertThat(forChange(r).get().requirements).containsExactly(READY);
-    verifyHasSubmitRecord(
-        forChange(r).get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.NEED);
-
-    changeApi.current().review(new ReviewInput().label(LabelId.VERIFIED, 1));
-    assertThat(changeApi.get().submittable).isTrue();
-    verifyHasSubmitRecord(
-        changeApi.get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireCodeOwnerApprovalEvenIfVerifiedWasApproved() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, admin2);
-
-    installVerifiedLabel();
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    assertThat(changeApi.get().submittable).isFalse();
-    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(new ReviewInput().label(LabelId.VERIFIED, 1));
-    ChangeInfo changeNotReady = forChange(r).get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-    verifyHasSubmitRecord(
-        changeNotReady.submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK);
-
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireConfiguredLabelByCodeOwner() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    String labelId = "Foo";
-    addOwnerFileToRoot(true, LabelDefinition.parse(labelId).get(), admin2);
-
-    installLabel(labelId);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    assertThat(changeApi.get().submittable).isFalse();
-    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeStillNotReady = changeApi.get();
-    assertThat(changeStillNotReady.submittable).isFalse();
-    assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(new ReviewInput().label(labelId, 1));
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-    verifyHasSubmitRecord(changeReady.submitRecords, labelId, SubmitRecordInfo.Label.Status.OK);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireConfiguredLabelByCodeOwnerEvenIfItIsNotConfiguredForProject()
-      throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    String notExistinglabelId = "Foo";
-    addOwnerFileToRoot(true, LabelDefinition.parse(notExistinglabelId).get(), admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    assertThat(changeApi.get().submittable).isFalse();
-    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeStillNotReady = changeApi.get();
-    assertThat(changeStillNotReady.submittable).isFalse();
-    assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireConfiguredLabelScoreByCodeOwner() throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
-    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
-    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.recommend());
-    ChangeInfo changeReadyWithOwnerScore = forChange(r).get();
-    assertThat(changeReadyWithOwnerScore.submittable).isTrue();
-    assertThat(changeReadyWithOwnerScore.requirements).containsExactly(READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldConfiguredLabelScoreByCodeOwnerBeNotSufficientIfLabelRequiresMaxValue()
-      throws Exception {
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.recommend());
-    ChangeInfo ownersVoteNotSufficient = changeApi.get();
-    assertThat(ownersVoteNotSufficient.submittable).isFalse();
-    assertThat(ownersVoteNotSufficient.requirements).containsExactly(READY);
-    verifyHasSubmitRecord(
-        ownersVoteNotSufficient.submitRecords,
-        LabelId.CODE_REVIEW,
-        SubmitRecordInfo.Label.Status.NEED);
-
-    requestScopeOperations.setApiUser(admin.id());
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReadyWithMaxScore = forChange(r).get();
-    assertThat(changeReadyWithMaxScore.submittable).isTrue();
-    assertThat(changeReadyWithMaxScore.requirements).containsExactly(READY);
-    verifyHasSubmitRecord(
-        changeReadyWithMaxScore.submitRecords,
-        LabelId.CODE_REVIEW,
-        SubmitRecordInfo.Label.Status.OK);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldConfiguredLabelScoreByCodeOwnersOverwriteSubmitRequirement() throws Exception {
-    installLabel(TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_OP).build());
-
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
-
-    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.recommend());
-    ChangeInfo ownersVoteSufficient = forChange(r).get();
-    assertThat(ownersVoteSufficient.submittable).isTrue();
-    assertThat(ownersVoteSufficient.requirements).containsExactly(READY);
-  }
-
-  @Test
-  @GlobalPluginConfig(
-      pluginName = "owners",
-      name = "owners.enableSubmitRequirement",
-      value = "true")
-  public void shouldRequireApprovalFromGrandParentProjectOwner() throws Exception {
-    Project.NameKey parentProjectName =
-        createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY);
-    Project.NameKey childProjectName =
-        createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY);
-    TestRepository<InMemoryRepository> childRepo = cloneProject(childProjectName);
-
-    TestAccount admin2 = accountCreator.admin2();
-    addOwnerFileToRefsMetaConfig(true, admin2, allProjects);
-
-    PushOneCommit.Result r =
-        createCommitAndPush(childRepo, "refs/for/master", "Add a file", "foo", "bar");
-    ChangeApi changeApi = forChange(r);
-    ChangeInfo changeNotReady = changeApi.get();
-    assertThat(changeNotReady.submittable).isFalse();
-    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
-
-    changeApi.current().review(ReviewInput.approve());
-    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
-    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
-    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
-
-    requestScopeOperations.setApiUser(admin2.id());
-    forChange(r).current().review(ReviewInput.approve());
-    ChangeInfo changeReady = forChange(r).get();
-    assertThat(changeReady.submittable).isTrue();
-    assertThat(changeReady.requirements).containsExactly(READY);
-  }
-
-  private void verifyHasSubmitRecord(
-      Collection<SubmitRecordInfo> records, String label, SubmitRecordInfo.Label.Status status) {
-    assertThat(
-            records.stream()
-                .flatMap(record -> record.labels.stream())
-                .filter(l -> l.label.equals(label) && l.status == status)
-                .findAny())
-        .isPresent();
-  }
-
-  private void installVerifiedLabel() throws Exception {
-    installLabel(LabelId.VERIFIED);
-  }
-
-  private void installLabel(String labelId) throws Exception {
-    LabelType verified =
-        labelBuilder(labelId, value(1, "Verified"), value(0, "No score"), value(-1, "Fails"))
-            .setFunction(LabelFunction.MAX_WITH_BLOCK)
-            .build();
-
-    installLabel(verified);
-
-    String heads = RefNames.REFS_HEADS + "*";
-    projectOperations
-        .project(project)
-        .forUpdate()
-        .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1))
-        .update();
-  }
-
-  private void installLabel(LabelType label) throws Exception {
-    try (ProjectConfigUpdate u = updateProject(project)) {
-      u.getConfig().upsertLabelType(label);
-      u.save();
-    }
-  }
-
-  protected ChangeApi forChange(PushOneCommit.Result r) throws RestApiException {
-    return gApi.changes().id(r.getChangeId());
-  }
-
-  private void addOwnerFileWithMatchersToRoot(
-      boolean inherit, String extension, TestAccount... users) throws Exception {
-    // Add OWNERS file to root:
-    //
-    // inherited: true
-    // matchers:
-    // - suffix: extension
-    //   owners:
-    //   - u1.email()
-    //   - ...
-    //   - uN.email()
-    pushOwnersToMaster(
-        String.format(
-            "inherited: %s\nmatchers:\n" + "- suffix: %s\n  owners:\n%s",
-            inherit,
-            extension,
-            Stream.of(users)
-                .map(user -> String.format("   - %s\n", user.email()))
-                .collect(joining())));
-  }
-
-  private void addOwnerFileToRoot(boolean inherit, TestAccount u) throws Exception {
-    // Add OWNERS file to root:
-    //
-    // inherited: true
-    // owners:
-    // - u.email()
-    pushOwnersToMaster(String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()));
-  }
-
-  private void addOwnerFileToRefsMetaConfig(
-      boolean inherit, TestAccount u, Project.NameKey projectName) throws Exception {
-    // Add OWNERS file to root:
-    //
-    // inherited: true
-    // owners:
-    // - u.email()
-    pushOwnersToRefsMetaConfig(
-        String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()), projectName);
-  }
-
-  protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
-      throws Exception {
-    // Add OWNERS file to root:
-    //
-    // inherited: true
-    // label: label,score # score is optional
-    // owners:
-    // - u.email()
-    pushOwnersToMaster(
-        String.format(
-            "inherited: %s\nlabel: %s\nowners:\n- %s\n",
-            inherit,
-            String.format(
-                "%s%s",
-                label.getName(),
-                label.getScore().map(value -> String.format(",%d", value)).orElse("")),
-            u.email()));
-  }
-
-  private void pushOwnersToMaster(String owners) throws Exception {
-    pushFactory
-        .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners)
-        .to(RefNames.fullName("master"))
-        .assertOkStatus();
-  }
-
-  private void pushOwnersToRefsMetaConfig(String owners, Project.NameKey projectName)
-      throws Exception {
-    TestRepository<InMemoryRepository> project = cloneProject(projectName);
-    GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
-    project.reset(RefNames.REFS_CONFIG);
-    pushFactory
-        .create(admin.newIdent(), project, "Add OWNER file", "OWNERS", owners)
-        .to(RefNames.REFS_CONFIG)
-        .assertOkStatus();
-  }
-}
+public class OwnersSubmitRequirementIT extends OwnersSubmitRequirementITAbstract {}
diff --git a/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java
new file mode 100644
index 0000000..d7cea60
--- /dev/null
+++ b/owners/src/test/java/com/googlesource/gerrit/owners/OwnersSubmitRequirementITAbstract.java
@@ -0,0 +1,515 @@
+// Copyright (C) 2023 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.googlesource.gerrit.owners;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+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.labelBuilder;
+import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static java.util.stream.Collectors.joining;
+
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GlobalPluginConfig;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRecordInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.project.testing.TestLabels;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.owners.common.LabelDefinition;
+import java.util.Collection;
+import java.util.stream.Stream;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+abstract class OwnersSubmitRequirementITAbstract extends LightweightPluginDaemonTest {
+  private static final LegacySubmitRequirementInfo NOT_READY =
+      new LegacySubmitRequirementInfo("NOT_READY", "Owners", "owners");
+  private static final LegacySubmitRequirementInfo READY =
+      new LegacySubmitRequirementInfo("OK", "Owners", "owners");
+
+  @Inject protected RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireAtLeastOneApprovalForMatchingPathFromOwner() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    TestAccount user1 = accountCreator.user1();
+    addOwnerFileWithMatchersToRoot(true, ".md", admin2, user1);
+
+    PushOneCommit.Result r = createChange("Add a file", "README.md", "foo");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldNotRequireApprovalForNotMatchingPath() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileWithMatchersToRoot(true, ".md", admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "README.txt", "foo");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).isEmpty();
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeReady = changeApi.get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).isEmpty();
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireApprovalFromRootOwner() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldBlockOwnersApprovalForMaxNegativeVote() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+
+    changeApi.current().review(ReviewInput.reject());
+    assertThat(forChange(r).get().submittable).isFalse();
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireVerifiedApprovalEvenIfCodeOwnerApproved() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, admin2);
+
+    installVerifiedLabel();
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    assertThat(changeApi.get().submittable).isFalse();
+    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    assertThat(forChange(r).get().submittable).isFalse();
+    assertThat(forChange(r).get().requirements).containsExactly(READY);
+    verifyHasSubmitRecord(
+        forChange(r).get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.NEED);
+
+    changeApi.current().review(new ReviewInput().label(LabelId.VERIFIED, 1));
+    assertThat(changeApi.get().submittable).isTrue();
+    verifyHasSubmitRecord(
+        changeApi.get().submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireCodeOwnerApprovalEvenIfVerifiedWasApproved() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, admin2);
+
+    installVerifiedLabel();
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    assertThat(changeApi.get().submittable).isFalse();
+    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(new ReviewInput().label(LabelId.VERIFIED, 1));
+    ChangeInfo changeNotReady = forChange(r).get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+    verifyHasSubmitRecord(
+        changeNotReady.submitRecords, LabelId.VERIFIED, SubmitRecordInfo.Label.Status.OK);
+
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireConfiguredLabelByCodeOwner() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    String labelId = "Foo";
+    addOwnerFileToRoot(true, LabelDefinition.parse(labelId).get(), admin2);
+
+    installLabel(labelId);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    assertThat(changeApi.get().submittable).isFalse();
+    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeStillNotReady = changeApi.get();
+    assertThat(changeStillNotReady.submittable).isFalse();
+    assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(new ReviewInput().label(labelId, 1));
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+    verifyHasSubmitRecord(changeReady.submitRecords, labelId, SubmitRecordInfo.Label.Status.OK);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireConfiguredLabelByCodeOwnerEvenIfItIsNotConfiguredForProject()
+      throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    String notExistinglabelId = "Foo";
+    addOwnerFileToRoot(true, LabelDefinition.parse(notExistinglabelId).get(), admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    assertThat(changeApi.get().submittable).isFalse();
+    assertThat(changeApi.get().requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeStillNotReady = changeApi.get();
+    assertThat(changeStillNotReady.submittable).isFalse();
+    assertThat(changeStillNotReady.requirements).containsExactly(NOT_READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireConfiguredLabelScoreByCodeOwner() throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.recommend());
+    ChangeInfo changeReadyWithOwnerScore = forChange(r).get();
+    assertThat(changeReadyWithOwnerScore.submittable).isTrue();
+    assertThat(changeReadyWithOwnerScore.requirements).containsExactly(READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldConfiguredLabelScoreByCodeOwnerBeNotSufficientIfLabelRequiresMaxValue()
+      throws Exception {
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.recommend());
+    ChangeInfo ownersVoteNotSufficient = changeApi.get();
+    assertThat(ownersVoteNotSufficient.submittable).isFalse();
+    assertThat(ownersVoteNotSufficient.requirements).containsExactly(READY);
+    verifyHasSubmitRecord(
+        ownersVoteNotSufficient.submitRecords,
+        LabelId.CODE_REVIEW,
+        SubmitRecordInfo.Label.Status.NEED);
+
+    requestScopeOperations.setApiUser(admin.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReadyWithMaxScore = forChange(r).get();
+    assertThat(changeReadyWithMaxScore.submittable).isTrue();
+    assertThat(changeReadyWithMaxScore.requirements).containsExactly(READY);
+    verifyHasSubmitRecord(
+        changeReadyWithMaxScore.submitRecords,
+        LabelId.CODE_REVIEW,
+        SubmitRecordInfo.Label.Status.OK);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldConfiguredLabelScoreByCodeOwnersOverwriteSubmitRequirement() throws Exception {
+    installLabel(TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_OP).build());
+
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRoot(true, LabelDefinition.parse("Code-Review,1").get(), admin2);
+
+    PushOneCommit.Result r = createChange("Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.recommend());
+    ChangeInfo ownersVoteSufficient = forChange(r).get();
+    assertThat(ownersVoteSufficient.submittable).isTrue();
+    assertThat(ownersVoteSufficient.requirements).containsExactly(READY);
+  }
+
+  @Test
+  @GlobalPluginConfig(
+      pluginName = "owners",
+      name = "owners.enableSubmitRequirement",
+      value = "true")
+  public void shouldRequireApprovalFromGrandParentProjectOwner() throws Exception {
+    Project.NameKey parentProjectName =
+        createProjectOverAPI("parent", allProjects, true, SubmitType.FAST_FORWARD_ONLY);
+    Project.NameKey childProjectName =
+        createProjectOverAPI("child", parentProjectName, true, SubmitType.FAST_FORWARD_ONLY);
+    TestRepository<InMemoryRepository> childRepo = cloneProject(childProjectName);
+
+    TestAccount admin2 = accountCreator.admin2();
+    addOwnerFileToRefsMetaConfig(true, admin2, allProjects);
+
+    PushOneCommit.Result r =
+        createCommitAndPush(childRepo, "refs/for/master", "Add a file", "foo", "bar");
+    ChangeApi changeApi = forChange(r);
+    ChangeInfo changeNotReady = changeApi.get();
+    assertThat(changeNotReady.submittable).isFalse();
+    assertThat(changeNotReady.requirements).containsExactly(NOT_READY);
+
+    changeApi.current().review(ReviewInput.approve());
+    ChangeInfo changeNotReadyAfterSelfApproval = changeApi.get();
+    assertThat(changeNotReadyAfterSelfApproval.submittable).isFalse();
+    assertThat(changeNotReadyAfterSelfApproval.requirements).containsExactly(NOT_READY);
+
+    requestScopeOperations.setApiUser(admin2.id());
+    forChange(r).current().review(ReviewInput.approve());
+    ChangeInfo changeReady = forChange(r).get();
+    assertThat(changeReady.submittable).isTrue();
+    assertThat(changeReady.requirements).containsExactly(READY);
+  }
+
+  private void verifyHasSubmitRecord(
+      Collection<SubmitRecordInfo> records, String label, SubmitRecordInfo.Label.Status status) {
+    assertThat(
+            records.stream()
+                .flatMap(record -> record.labels.stream())
+                .filter(l -> l.label.equals(label) && l.status == status)
+                .findAny())
+        .isPresent();
+  }
+
+  private void installVerifiedLabel() throws Exception {
+    installLabel(LabelId.VERIFIED);
+  }
+
+  private void installLabel(String labelId) throws Exception {
+    LabelType verified =
+        labelBuilder(labelId, value(1, "Verified"), value(0, "No score"), value(-1, "Fails"))
+            .setFunction(LabelFunction.MAX_WITH_BLOCK)
+            .build();
+
+    installLabel(verified);
+
+    String heads = RefNames.REFS_HEADS + "*";
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(verified.getName()).ref(heads).group(REGISTERED_USERS).range(-1, 1))
+        .update();
+  }
+
+  private void installLabel(LabelType label) throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().upsertLabelType(label);
+      u.save();
+    }
+  }
+
+  protected ChangeApi forChange(PushOneCommit.Result r) throws RestApiException {
+    return gApi.changes().id(r.getChangeId());
+  }
+
+  private void addOwnerFileWithMatchersToRoot(
+      boolean inherit, String extension, TestAccount... users) throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // matchers:
+    // - suffix: extension
+    //   owners:
+    //   - u1.email()
+    //   - ...
+    //   - uN.email()
+    pushOwnersToMaster(
+        String.format(
+            "inherited: %s\nmatchers:\n" + "- suffix: %s\n  owners:\n%s",
+            inherit,
+            extension,
+            Stream.of(users)
+                .map(user -> String.format("   - %s\n", user.email()))
+                .collect(joining())));
+  }
+
+  private void addOwnerFileToRoot(boolean inherit, TestAccount u) throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // owners:
+    // - u.email()
+    pushOwnersToMaster(String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()));
+  }
+
+  private void addOwnerFileToRefsMetaConfig(
+      boolean inherit, TestAccount u, Project.NameKey projectName) throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // owners:
+    // - u.email()
+    pushOwnersToRefsMetaConfig(
+        String.format("inherited: %s\nowners:\n- %s\n", inherit, u.email()), projectName);
+  }
+
+  protected void addOwnerFileToRoot(boolean inherit, LabelDefinition label, TestAccount u)
+      throws Exception {
+    // Add OWNERS file to root:
+    //
+    // inherited: true
+    // label: label,score # score is optional
+    // owners:
+    // - u.email()
+    pushOwnersToMaster(
+        String.format(
+            "inherited: %s\nlabel: %s\nowners:\n- %s\n",
+            inherit,
+            String.format(
+                "%s%s",
+                label.getName(),
+                label.getScore().map(value -> String.format(",%d", value)).orElse("")),
+            u.email()));
+  }
+
+  private void pushOwnersToMaster(String owners) throws Exception {
+    pushFactory
+        .create(admin.newIdent(), testRepo, "Add OWNER file", "OWNERS", owners)
+        .to(RefNames.fullName("master"))
+        .assertOkStatus();
+  }
+
+  private void pushOwnersToRefsMetaConfig(String owners, Project.NameKey projectName)
+      throws Exception {
+    TestRepository<InMemoryRepository> project = cloneProject(projectName);
+    GitUtil.fetch(project, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
+    project.reset(RefNames.REFS_CONFIG);
+    pushFactory
+        .create(admin.newIdent(), project, "Add OWNER file", "OWNERS", owners)
+        .to(RefNames.REFS_CONFIG)
+        .assertOkStatus();
+  }
+}