Check read permission in getRepoFile

* Inject PermissionBackend to all API entry points and
  pass it to hasReadAccess.
* Add new ACL read tests in IncludeIT and OwnersValidatorIT;
  share code in FindOwners.
* Improve readability in IncludeIT and ParserTest.

Change-Id: Ia10601eb00980b62f08c240b91da81ee0c38d0df
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
index d6a182d..7a654da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Action.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -56,6 +57,7 @@
   private final Emails emails;
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
+  private final PermissionBackend permissionBackend;
   private final PluginConfigFactory configFactory;
   private final Provider<CurrentUser> userProvider;
   private final ProjectCache projectCache;
@@ -69,6 +71,7 @@
 
   @Inject
   Action(
+      PermissionBackend permissionBackend,
       PluginConfigFactory configFactory,
       Provider<CurrentUser> userProvider,
       ChangeData.Factory changeDataFactory,
@@ -76,6 +79,7 @@
       Emails emails,
       GitRepositoryManager repoManager,
       ProjectCache projectCache) {
+    this.permissionBackend = permissionBackend;
     this.userProvider = userProvider;
     this.changeDataFactory = changeDataFactory;
     this.accountCache = accountCache;
@@ -163,6 +167,7 @@
         Cache.getInstance(configFactory, repoManager)
             .get(
                 useCache,
+                permissionBackend,
                 projectState,
                 accountCache,
                 emails,
@@ -222,6 +227,7 @@
             Cache.getInstance(configFactory, repoManager)
                 .get(
                     true, // use cached OwnersDb
+                    permissionBackend,
                     projectCache.get(resource.getProject()),
                     accountCache,
                     emails,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
index 0c2af59..7d2cfa5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Cache.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.Collection;
@@ -91,6 +92,7 @@
   /** Returns a cached or new OwnersDb, for the current patchset. */
   OwnersDb get(
       Boolean useCache,
+      PermissionBackend permissionBackend,
       ProjectState projectState,
       AccountCache accountCache,
       Emails emails,
@@ -100,6 +102,7 @@
       throws StorageException {
     return get(
         useCache,
+        permissionBackend,
         projectState,
         accountCache,
         emails,
@@ -112,6 +115,7 @@
   /** Returns a cached or new OwnersDb, for the specified patchset. */
   OwnersDb get(
       Boolean useCache,
+      PermissionBackend permissionBackend,
       ProjectState projectState,
       AccountCache accountCache,
       Emails emails,
@@ -125,6 +129,7 @@
     // TODO: get changed files of the given patchset?
     return get(
         useCache,
+        permissionBackend,
         projectState,
         accountCache,
         emails,
@@ -139,6 +144,7 @@
   /** Returns a cached or new OwnersDb, for the specified branch and changed files. */
   OwnersDb get(
       Boolean useCache,
+      PermissionBackend permissionBackend,
       ProjectState projectState,
       AccountCache accountCache,
       Emails emails,
@@ -151,6 +157,7 @@
     if (dbCache == null || !useCache) { // Do not cache OwnersDb
       logger.atFiner().log("Create new OwnersDb, key=%s", key);
       return new OwnersDb(
+          permissionBackend,
           projectState,
           accountCache,
           emails,
@@ -171,6 +178,7 @@
             public OwnersDb call() {
               logger.atFiner().log("Create new OwnersDb, key=%s", key);
               return new OwnersDb(
+                  permissionBackend,
                   projectState,
                   accountCache,
                   emails,
@@ -186,6 +194,7 @@
       logger.atSevere().withCause(e).log(
           "Cache.get has exception for %s", Config.getChangeId(changeData));
       return new OwnersDb(
+          permissionBackend,
           projectState,
           accountCache,
           emails,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
index 6399771..2a7ffd9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Checker.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.StoredValues;
@@ -40,6 +41,7 @@
   private static final String EXEMPT_MESSAGE2 = "Exempted-From-Owner-Approval:";
 
   private final GitRepositoryManager repoManager;
+  private final PermissionBackend permissionBackend;
   private final PluginConfigFactory configFactory;
   private final Config config;
   private final ProjectState projectState; // could be null when used by FindOwnersIT
@@ -48,11 +50,13 @@
 
   Checker(
       GitRepositoryManager repoManager,
+      PermissionBackend permissionBackend,
       PluginConfigFactory configFactory,
       ProjectState projectState,
       ChangeData changeData,
       int v) {
     this.repoManager = repoManager;
+    this.permissionBackend = permissionBackend;
     this.configFactory = configFactory;
     this.projectState = projectState;
     this.changeData = changeData;
@@ -128,6 +132,7 @@
       Checker checker =
           new Checker(
               StoredValues.REPO_MANAGER.get(engine),
+              StoredValues.PERMISSION_BACKEND.get(engine),
               StoredValues.PLUGIN_CONFIG_FACTORY.get(engine),
               StoredValues.PROJECT_STATE.get(engine),
               changeData,
@@ -149,7 +154,15 @@
     // many times. So this function should use cached values.
     OwnersDb db =
         Cache.getInstance(configFactory, repoManager)
-            .get(true, projectState, accountCache, emails, repoManager, configFactory, changeData);
+            .get(
+                true,
+                permissionBackend,
+                projectState,
+                accountCache,
+                emails,
+                repoManager,
+                configFactory,
+                changeData);
     if (db.getNumOwners() <= 0) {
       return 0;
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
index 5735ced..ce3fdef 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/GetOwners.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
@@ -52,6 +53,7 @@
 
   @Inject
   GetOwners(
+      PermissionBackend permissionBackend,
       PluginConfigFactory configFactory,
       Provider<CurrentUser> userProvider,
       ChangeData.Factory dataFactory,
@@ -61,6 +63,7 @@
       ProjectCache projectCache) {
     this.action =
         new Action(
+            permissionBackend,
             configFactory,
             userProvider,
             dataFactory,
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
index 7058842..cfa46f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -19,12 +19,16 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.net.InetAddress;
@@ -52,6 +56,7 @@
 class OwnersDb {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final PermissionBackend permissionBackend;
   private final AccountCache accountCache;
   private final GitRepositoryManager repoManager;
   private final Emails emails;
@@ -71,6 +76,7 @@
   List<String> logs = new ArrayList<>(); // trace/debug messages
 
   OwnersDb(
+      PermissionBackend permissionBackend,
       ProjectState projectState,
       AccountCache accountCache,
       Emails emails,
@@ -80,6 +86,7 @@
       ChangeData changeData,
       String branch,
       Collection<String> files) {
+    this.permissionBackend = permissionBackend;
     this.accountCache = accountCache;
     this.repoManager = repoManager;
     this.emails = emails;
@@ -111,7 +118,17 @@
           // this project should have a non-empty root file of that name.
           // We added this requirement to detect errors in project config files
           // and Gerrit server bugs that return wrong value of "ownersFileName".
-          String content = getFile(readFiles, repo, projectName, id, "/" + ownersFileName, logs);
+          String content =
+              getRepoFile(
+                  permissionBackend,
+                  readFiles,
+                  null,
+                  repo,
+                  id,
+                  projectName,
+                  branch,
+                  "/" + ownersFileName,
+                  logs);
           String found = "Found";
           if (content.isEmpty()) {
             String changeId = Config.getChangeId(changeData);
@@ -138,7 +155,17 @@
             readDirs.add(dir);
             logs.add("findOwnersFileIn:" + dir);
             String filePath = dir + "/" + ownersFileName;
-            String content = getFile(readFiles, repo, projectName, id, filePath, logs);
+            String content =
+                getRepoFile(
+                    permissionBackend,
+                    readFiles,
+                    null,
+                    repo,
+                    id,
+                    projectName,
+                    branch,
+                    filePath,
+                    logs);
             if (content != null && !content.isEmpty()) {
               addFile(
                   readFiles,
@@ -244,7 +271,8 @@
       String dirPath,
       String filePath,
       String[] lines) {
-    Parser parser = new Parser(readFiles, repoManager, project, branch, filePath, logs);
+    Parser parser =
+        new Parser(permissionBackend, readFiles, repoManager, project, branch, filePath, logs);
     Parser.Result result = parser.parseFile(dirPath, lines);
     if (result.stopLooking) {
       stopLooking.add(dirPath);
@@ -416,10 +444,36 @@
     }
   }
 
+  private static boolean hasReadAccess(
+      PermissionBackend permissionBackend, String project, String branch, List<String> logs) {
+    if (permissionBackend == null || branch == null || project == null) {
+      return true; // cannot check, so assume okay
+    }
+    if (!branch.startsWith("refs/")) {
+      branch = "refs/heads/" + branch;
+    }
+    try {
+      permissionBackend
+          .currentUser()
+          .project(Project.nameKey(project))
+          .ref(branch)
+          .check(RefPermission.READ);
+    } catch (AuthException | PermissionBackendException e) {
+      logger.atSevere().withCause(e).log(
+          "getFile cannot read file in project %s branch %s", project, branch);
+      logException(logs, "hasReadAccess", e);
+      return false;
+    }
+    return true;
+  }
+
   /** Returns file content or empty string; uses project+branch+file names. */
   public static String getRepoFile(
+      PermissionBackend permissionBackend,
       Map<String, String> readFiles,
       GitRepositoryManager repoManager,
+      Repository repository,
+      ObjectId id,
       String project,
       String branch,
       String file,
@@ -429,51 +483,48 @@
     file = Util.gitRepoFilePath(file);
     String content = findReadFile(readFiles, project, file);
     if (content == null) {
+      if (!hasReadAccess(permissionBackend, project, branch, logs)) {
+        return ""; // treat as read error
+      }
       content = "";
-      if (repoManager != null) { // ParserTest can call with null repoManager
+      if ((id == null || repository == null) && repoManager != null) {
+        // create ObjectId from repoManager
         try (Repository repo = repoManager.openRepository(Project.nameKey(project))) {
-          ObjectId id = repo.resolve(branch);
+          id = repo.resolve(branch);
           if (id != null) {
-            return getFile(readFiles, repo, project, id, file, logs);
+            content = getFile(repo, id, file, logs);
+          } else {
+            logs.add("getRepoFile not found branch " + branch);
           }
-          logs.add("getRepoFile not found branch " + branch);
         } catch (Exception e) {
           logger.atSevere().log("getRepoFile failed to find repository of project %s", project);
           logException(logs, "getRepoFile", e);
         }
+      } else if (id != null && repository != null) {
+        content = getFile(repository, id, file, logs);
       }
+      saveReadFile(readFiles, project, file, content);
     }
     return content;
   }
 
   /** Returns file content or empty string; uses Repository. */
-  private static String getFile(
-      Map<String, String> readFiles,
-      Repository repo,
-      String project,
-      ObjectId id,
-      String file,
-      List<String> logs) {
-    file = Util.gitRepoFilePath(file);
-    String content = findReadFile(readFiles, project, file);
-    if (content == null) {
-      content = "";
-      try (RevWalk revWalk = new RevWalk(repo)) {
-        String header = "getFile:" + file;
-        RevTree tree = revWalk.parseCommit(id).getTree();
-        ObjectReader reader = revWalk.getObjectReader();
-        TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
-        if (treeWalk != null) {
-          content = new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
-          logs.add(header + ":(...)");
-        } else {
-          logs.add(header + " (NOT FOUND)");
-        }
-      } catch (Exception e) {
-        logger.atSevere().withCause(e).log("get file %s", file);
-        logException(logs, "getFile", e);
+  private static String getFile(Repository repo, ObjectId id, String file, List<String> logs) {
+    String content = "";
+    try (RevWalk revWalk = new RevWalk(repo)) {
+      String header = "getFile:" + file;
+      RevTree tree = revWalk.parseCommit(id).getTree();
+      ObjectReader reader = revWalk.getObjectReader();
+      TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
+      if (treeWalk != null) {
+        content = new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
+        logs.add(header + ":(...)");
+      } else {
+        logs.add(header + " (NOT FOUND)");
       }
-      saveReadFile(readFiles, project, file, content);
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log("get file %s", file);
+      logException(logs, "getFile", e);
     }
     return content;
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
index 11559df..f5586b1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -88,6 +89,7 @@
   }
 
   private final String pluginName;
+  private final PermissionBackend permissionBackend;
   private final PluginConfigFactory cfgFactory;
   private final GitRepositoryManager repoManager;
   private final Emails emails;
@@ -95,12 +97,14 @@
   @Inject
   OwnersValidator(
       @PluginName String pluginName,
+      PermissionBackend permissionBackend,
       PluginConfigFactory cfgFactory,
       GitRepositoryManager repoManager,
       Emails emails) {
     this.pluginName = pluginName;
-    this.repoManager = repoManager;
+    this.permissionBackend = permissionBackend;
     this.cfgFactory = cfgFactory;
+    this.repoManager = repoManager;
     this.emails = emails;
   }
 
@@ -130,7 +134,7 @@
   @Override
   public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent event)
       throws CommitValidationException {
-    Checker checker = new Checker(event, false);
+    Checker checker = new Checker(event, false, permissionBackend);
     try {
       Project.NameKey project = event.project.getNameKey();
       PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(project, pluginName);
@@ -154,6 +158,7 @@
     // An inner class to keep needed data specific to one commit event.
     CommitReceivedEvent event;
     boolean verbose;
+    PermissionBackend permissionBackend;
     List<CommitValidationMessage> messages;
     Map<String, ObjectId> allFiles; // changedFilePath => ObjectId
     Map<String, String> readFiles; // project:file => content
@@ -161,9 +166,10 @@
     // Collect all email addresses from all files and check each address only once.
     Map<String, Set<String>> email2lines;
 
-    Checker(CommitReceivedEvent event, boolean verbose) {
+    Checker(CommitReceivedEvent event, boolean verbose, PermissionBackend permissionBackend) {
       this.event = event;
       this.verbose = verbose;
+      this.permissionBackend = permissionBackend;
       messages = new ArrayList<>();
       readFiles = new HashMap<>();
       checkedFiles = new HashSet<>();
@@ -353,8 +359,16 @@
       addVerboseMsg("check repo file " + key);
       String content =
           OwnersDb.getRepoFile(
-              readFiles, repoManager, KPF[1], event.refName, repoFile, new ArrayList<>());
-      if (isNullOrEmpty(content)) {
+              permissionBackend,
+              readFiles,
+              repoManager,
+              null,
+              null,
+              KPF[1],
+              event.refName,
+              repoFile,
+              new ArrayList<>());
+      if (isNullOrEmpty(content)) { // file not found or not readable.
         addVerboseMsg("cannot find file: " + key);
         // unchecked: including-file-path : line number : source line
         addMsg("unchecked: " + qualifiedPath(project, path) + ":" + num + ": " + directive);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
index faf6141..b3ecda3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import java.io.IOException;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -31,12 +32,17 @@
 
 /**
  * Parse lines in an OWNERS file and put them into an OwnersDb. One Parser object should be created
- * to parse only one OWNERS file. It keeps repoManager, project, branch, and filePath of the OWNERS
- * file so it can find files that are included by OWNERS.
+ * to parse only one OWNERS file. It keeps permissionBackend, readFiles, repoManager, project,
+ * branch, and filePath of the OWNERS file so it can find files that are included by OWNERS.
  *
- * <p>The usage pattern is: Parser parser = new Parser(repoManager, project, branch, repoFilePath);
- * String content = OwnersDb.getRepoFile(readFiles, repoManager, project, branch, repoFilePath,
- * logs); Parser.Result result = parser.parseFile(dirPath, content);
+ * <p>The usage pattern is:
+ *
+ * <pre>
+ *   Parser parser = new Parser(permissionBackend, readFiles, repoManager, project, branch, repoFilePath);
+ *   String content = OwnersDb.getRepoFile(permissionBackend, readFiles, repoManager, null, null,
+ *                                         project, branch, repoFilePath, logs);
+ *   Parser.Result result = parser.parseFile(dirPath, content);
+ * </pre>
  *
  * <p>OWNERS file syntax, semantics, and examples are included in syntax.md.
  */
@@ -95,8 +101,9 @@
   private static final Pattern PAT_INCLUDE_OR_FILE =
       Pattern.compile("^.*(" + INCLUDE_OR_FILE + PROJECT_AND_FILE + ")" + EOL);
 
-  // A parser keeps current readFiles, repoManager, project, branch,
+  // A parser keeps current permissionBackend, readFiles, repoManager, project, branch,
   // included file path, and debug/trace logs.
+  private final PermissionBackend permissionBackend;
   private Map<String, String> readFiles;
   private GitRepositoryManager repoManager;
   private String branch; // All owners files are read from the same branch.
@@ -141,32 +148,30 @@
     }
   }
 
+  // For simple unit tests without a repository.
+  Parser(String project, String branch, String file) {
+    this(null, null, null, project, branch, file, new ArrayList<>());
+  }
+
   Parser(
+      PermissionBackend permissionBackend,
       Map<String, String> readFiles,
       GitRepositoryManager repoManager,
       String project,
       String branch,
       String file) {
-    init(readFiles, repoManager, project, branch, file, new ArrayList<>());
+    this(permissionBackend, readFiles, repoManager, project, branch, file, new ArrayList<>());
   }
 
   Parser(
+      PermissionBackend permissionBackend,
       Map<String, String> readFiles,
       GitRepositoryManager repoManager,
       String project,
       String branch,
       String file,
       List<String> logs) {
-    init(readFiles, repoManager, project, branch, file, logs);
-  }
-
-  private void init(
-      Map<String, String> readFiles,
-      GitRepositoryManager repoManager,
-      String project,
-      String branch,
-      String file,
-      List<String> logs) {
+    this.permissionBackend = permissionBackend;
     this.readFiles = readFiles;
     this.repoManager = repoManager;
     this.branch = branch;
@@ -405,7 +410,16 @@
       stack.push(project, repoFile);
       logs.add("parseLine:" + includeKPF);
       String content =
-          OwnersDb.getRepoFile(readFiles, repoManager, project, branch, repoFile, logs);
+          OwnersDb.getRepoFile(
+              permissionBackend,
+              readFiles,
+              repoManager,
+              null,
+              null,
+              project,
+              branch,
+              repoFile,
+              logs);
       if (content != null && !content.isEmpty()) {
         includedFileResult = parseFile("", content);
       } else {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
index 8e82a1d..9c487cb 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ApiIT.java
@@ -17,7 +17,6 @@
 import static com.google.common.truth.OptionalSubject.optionals;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
@@ -110,7 +109,14 @@
     Action.Parameters param = new Action.Parameters();
     Action action =
         new Action(
-            pluginConfig, null, changeDataFactory, accountCache, emails, repoManager, projectCache);
+            permissionBackend,
+            pluginConfig,
+            null,
+            changeDataFactory,
+            accountCache,
+            emails,
+            repoManager,
+            projectCache);
     Response<RestResult> response = action.apply(cr, param);
     RestResult result = response.value();
     verifyRestResult(result, 1, 1, changeInfo._number, false);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
index 9e3e3d2..00e7217 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwners.java
@@ -15,13 +15,16 @@
 package com.googlesource.gerrit.plugins.findowners;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -31,6 +34,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.Inject;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -45,6 +49,7 @@
 public abstract class FindOwners extends LightweightPluginDaemonTest {
 
   @Inject protected Emails emails;
+  @Inject protected PermissionBackend permissionBackend;
   @Inject protected ProjectOperations projectOperations;
 
   protected static final String PLUGIN_NAME = "find-owners";
@@ -112,6 +117,14 @@
     testRepo = cloneProject(project);
   }
 
+  protected void blockRead(Project.NameKey p) throws Exception {
+    projectOperations
+        .project(p)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+        .update();
+  }
+
   protected org.eclipse.jgit.lib.Config readProjectConfig() throws Exception {
     git().fetch().setRefSpecs(new RefSpec(REFS_CONFIG + ":" + REFS_CONFIG)).call();
     testRepo.reset(RefNames.REFS_CONFIG);
@@ -158,6 +171,7 @@
     OwnersDb db =
         cache.get(
             true,
+            permissionBackend,
             projectCache.get(project),
             accountCache,
             emails,
@@ -165,7 +179,7 @@
             pluginConfig,
             r.getChange(),
             1);
-    Checker c = new Checker(repoManager, pluginConfig, null, r.getChange(), 1);
+    Checker c = new Checker(repoManager, permissionBackend, pluginConfig, null, r.getChange(), 1);
     return c.findApproval(accountCache, db);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/IncludeIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/IncludeIT.java
index f5829a2..be64ad3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/IncludeIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/IncludeIT.java
@@ -29,6 +29,18 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   @Rule public Watcher watcher = new Watcher(logger);
 
+  private String getRepoFileLog(String msg1, String msg2) {
+    return "getRepoFile:" + msg1 + ", getFile:" + msg2 + ", ";
+  }
+
+  private String concat(String s1, String s2) {
+    return s1 + s2;
+  }
+
+  private String concat(String s1, String s2, String s3) {
+    return s1 + s2 + s3;
+  }
+
   @Test
   public void includeNotFoundTest() throws Exception {
     // c2 and c1 are both submitted before existence of OWNERS.
@@ -43,25 +55,18 @@
     String owner2paths = "owner2paths:{ a@a:[ ./ ], x@x:[ ./ ] }";
     String projectName = project.get();
     String expectedInLog =
-        "project:"
-            + projectName
-            + ", "
+        concat("project:", projectName, ", ")
             + "ownersFileName:OWNERS, "
             + "getBranchId:refs/heads/master(FOUND), "
             + "findOwnersFileFor:./t.c, "
             + "findOwnersFileIn:., "
-            + "getFile:OWNERS:(...), "
+            + getRepoFileLog(projectName + ":refs/heads/master:./OWNERS", "OWNERS:(...)")
             + "parseLine:include:P1/P2:f1, "
             + "getRepoFile:P1/P2:refs/heads/master:f1, "
-            + "getRepoFileException:repositorynotfound:P1/P2, " // repository not found
+            + "hasReadAccessException:project\\u0027P1/P2\\u0027isunavailable, " // cannot read
             + "parseLine:include:(), " // missing file is treated as empty
-            + "parseLine:include:"
-            + projectName
-            + ":./d1/d2/../../f2, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f2, "
-            + "getFile:f2(NOTFOUND), " // same repository but f2 is missing
+            + concat("parseLine:include:", projectName, ":./d1/d2/../../f2, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f2", "f2(NOTFOUND)")
             + "parseLine:include:(), " // missing file is treated as empty
             + "countNumOwners, "
             + "findOwners, "
@@ -101,25 +106,18 @@
     String owner2paths = "owner2paths:{ a@a:[ ./ ], g1@g:[ ./ ], g2@g:[ ./ ], x@x:[ ./ ] }";
     String projectName = project.get();
     String expectedInLog =
-        "project:"
-            + projectName
-            + ", "
+        concat("project:", projectName, ", ")
             + "ownersFileName:OWNERS, "
             + "getBranchId:refs/heads/master(FOUND), "
             + "findOwnersFileFor:./t.c, "
             + "findOwnersFileIn:., "
-            + "getFile:OWNERS:(...), "
+            + getRepoFileLog(projectName + ":refs/heads/master:./OWNERS", "OWNERS:(...)")
             + "parseLine:include:P1/P2:f1, "
             + "getRepoFile:P1/P2:refs/heads/master:f1, "
-            + "getRepoFileException:repositorynotfound:P1/P2, "
+            + "hasReadAccessException:project\\u0027P1/P2\\u0027isunavailable, "
             + "parseLine:include:(), " // P1/P2 is still not found
-            + "parseLine:include:"
-            + projectName
-            + ":./d1/d2/../../f2, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f2, "
-            + "getFile:f2:(...), " // f2 is included
+            + concat("parseLine:include:", projectName, ":./d1/d2/../../f2, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f2", "f2:(...)")
             + "countNumOwners, "
             + "findOwners, "
             + "checkFile:./t.c, "
@@ -162,14 +160,10 @@
     assertThat(getOwnersResponse(c1))
         .contains(
             "owners:[ "
-                + ownerD2
-                + ", "
-                + ownerF2
-                + ", "
-                + ownerD3
-                + ", "
-                + ownerX
-                + " ], files:[ d3/t.c ]");
+                + concat(ownerD2, ", ")
+                + concat(ownerF2, ", ")
+                + concat(ownerD3, ", ")
+                + concat(ownerX, " ], files:[ d3/t.c ]"));
   }
 
   @Test
@@ -247,45 +241,21 @@
     String response = getOwnersDebugResponse(c);
     String projectName = project.get();
     String expectedInLog =
-        "project:"
-            + projectName
-            + ", "
+        concat("project:", projectName, ", ")
             + "ownersFileName:OWNERS, "
             + "getBranchId:refs/heads/master(FOUND), "
             + "findOwnersFileFor:./t.c, "
             + "findOwnersFileIn:., "
-            + "getFile:OWNERS:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":./d1/../f1, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f1, "
-            + "getFile:f1:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":./f2, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f2, "
-            + "getFile:f2:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":d1/../f3, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f3, "
-            + "getFile:f3:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":/f4, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:f4, "
-            + "getFile:f4:(...), "
-            + "parseLine:errorRecursion:include:"
-            + projectName
-            + ":d2/../f2, "
+            + getRepoFileLog(projectName + ":refs/heads/master:./OWNERS", "OWNERS:(...)")
+            + concat("parseLine:include:", projectName, ":./d1/../f1, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f1", "f1:(...)")
+            + concat("parseLine:include:", projectName, ":./f2, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f2", "f2:(...)")
+            + concat("parseLine:include:", projectName, ":d1/../f3, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f3", "f3:(...)")
+            + concat("parseLine:include:", projectName, ":/f4, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:f4", "f4:(...)")
+            + concat("parseLine:errorRecursion:include:", projectName, ":d2/../f2, ")
             + "countNumOwners, "
             + "findOwners, "
             + "checkFile:./t.c, "
@@ -323,76 +293,32 @@
       assertThat(result).contains(skipLog + path);
     }
     String expectedInLog =
-        "project:"
-            + projectName
-            + ", "
+        concat("project:", projectName, ", ")
             + "ownersFileName:OWNERS, "
             + "getBranchId:refs/heads/master(FOUND), "
             + "findOwnersFileFor:./d6/OWNERS, "
             + "findOwnersFileIn:./d6, "
-            + "getFile:d6/OWNERS:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":/d0/f0, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d0/f0, "
-            + "getFile:d0/f0:(...), "
-            + "parseLine:include:"
-            + projectName
-            + ":../d1/d2/f1, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d1/d2/f1, "
-            + "getFile:d1/d2/f1:(...), "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":../../d0/f0, "
-            + "parseLine:include:"
-            + projectName
-            + ":../d2/f2, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d2/f2, "
-            + "getFile:d2/f2:(...), "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":../d0/f0, "
-            + "parseLine:include:"
-            + projectName
-            + ":/d2/d3/f3, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d2/d3/f3, "
-            + "getFile:d2/d3/f3:(...), "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":/d0/f0, "
-            + "parseLine:include:"
-            + projectName
-            + ":/d2/../d4/d5/f5, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d4/d5/f5, "
-            + "getFile:d4/d5/f5:(...), "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":/d2/f2, "
-            + "parseLine:include:"
-            + projectName
-            + ":../f4, "
-            + "getRepoFile:"
-            + projectName
-            + ":refs/heads/master:d4/f4, "
-            + "getFile:d4/f4:(...), "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":../d2/f2, "
-            + "parseLine:useSaved:include:"
-            + projectName
-            + ":/d4/f4, "
+            + getRepoFileLog(projectName + ":refs/heads/master:./d6/OWNERS", "d6/OWNERS:(...)")
+            + concat("parseLine:include:", projectName, ":/d0/f0, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d0/f0", "d0/f0:(...)")
+            + concat("parseLine:include:", projectName, ":../d1/d2/f1, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d1/d2/f1", "d1/d2/f1:(...)")
+            + concat("parseLine:useSaved:include:", projectName, ":../../d0/f0, ")
+            + concat("parseLine:include:", projectName, ":../d2/f2, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d2/f2", "d2/f2:(...)")
+            + concat("parseLine:useSaved:include:", projectName, ":../d0/f0, ")
+            + concat("parseLine:include:", projectName, ":/d2/d3/f3, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d2/d3/f3", "d2/d3/f3:(...)")
+            + concat("parseLine:useSaved:include:", projectName, ":/d0/f0, ")
+            + concat("parseLine:include:", projectName, ":/d2/../d4/d5/f5, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d4/d5/f5", "d4/d5/f5:(...)")
+            + concat("parseLine:useSaved:include:", projectName, ":/d2/f2, ")
+            + concat("parseLine:include:", projectName, ":../f4, ")
+            + getRepoFileLog(projectName + ":refs/heads/master:d4/f4", "d4/f4:(...)")
+            + concat("parseLine:useSaved:include:", projectName, ":../d2/f2, ")
+            + concat("parseLine:useSaved:include:", projectName, ":/d4/f4, ")
             + "findOwnersFileIn:., "
-            + "getFile:OWNERS(NOTFOUND), "
+            + getRepoFileLog(projectName + ":refs/heads/master:./OWNERS", "OWNERS(NOTFOUND)")
             + "countNumOwners, "
             + "findOwners, "
             + "checkFile:./d6/OWNERS, "
@@ -426,20 +352,46 @@
     // inherited: pA:OWNERS
     String owners =
         "owners:[ "
-            + ownerJson("pAd1f1@g")
-            + ", "
-            + ownerJson("pAd2@g")
-            + ", "
-            + ownerJson("pAf1@g")
-            + ", "
-            + ownerJson("pBd1f1@g")
-            + ", "
-            + ownerJson("pBd2f2@g")
-            + ", "
-            + ownerJson("pBf1@g")
-            + ", "
-            + ownerJson("pA@g", 0, 1, 0)
-            + " ]";
+            + concat(ownerJson("pAd1f1@g"), ", ")
+            + concat(ownerJson("pAd2@g"), ", ")
+            + concat(ownerJson("pAf1@g"), ", ")
+            + concat(ownerJson("pBd1f1@g"), ", ")
+            + concat(ownerJson("pBd2f2@g"), ", ")
+            + concat(ownerJson("pBf1@g"), ", ")
+            + concat(ownerJson("pA@g", 0, 1, 0), " ]");
+    assertThat(getOwnersResponse(c1)).contains(owners);
+  }
+
+  @Test
+  public void includeProjectOwnerACLTest() throws Exception {
+    // Test include directive with other unreadable project.
+    Project.NameKey pA = newProject("PA2");
+    Project.NameKey pB = newProject("PB2");
+    String nameA = pA.get();
+    String nameB = pB.get();
+    switchProject(pA);
+    addFile("1", "f1", "pAf1@g\ninclude ./d1/f1\n");
+    addFile("2", "d1/f1", "pAd1f1@g\ninclude " + nameB + ":" + "/d2/f2\n");
+    addFile("3", "d2/OWNERS", "pAd2@g\n  include " + nameA + "  : " + "../f1\n");
+    addFile("4", "OWNERS", "pA@g\n");
+    switchProject(pB);
+    addFile("5", "f1", "pBf1@g\ninclude ./d1/f1\n");
+    addFile("6", "f2", "pBf2@g\n");
+    addFile("7", "d1/f1", "pBd1f1@g\n");
+    addFile("8", "d2/f2", "pBd2f2@g\ninclude ../f1\n");
+    blockRead(project); // now cannot read pB
+    switchProject(pA);
+    PushOneCommit.Result c1 = createChange("c1", "d2/t.c", "Hello!");
+    // included: pA:d2/OWNERS, pA:d2/../f1, pA:d1/f1
+    // inherited: pA:OWNERS
+    // pB's OWNERS files are not readable
+    String owners =
+        "owners:[ "
+            + concat(ownerJson("pAd1f1@g"), ", ")
+            + concat(ownerJson("pAd2@g"), ", ")
+            + concat(ownerJson("pAf1@g"), ", ")
+            + concat(ownerJson("pA@g", 0, 1, 0), " ]");
+    // The "owners:[...]" substring contains only owners from pA.
     assertThat(getOwnersResponse(c1)).contains(owners);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
index f46b7ad..276e741 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorIT.java
@@ -25,10 +25,8 @@
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestPlugin;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
@@ -36,7 +34,6 @@
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
-import com.google.inject.Inject;
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -62,25 +59,10 @@
 
 /** Test OwnersValidator, which checks syntax of changed OWNERS files. */
 @TestPlugin(name = "find-owners", sysModule = "com.googlesource.gerrit.plugins.findowners.Module")
-public class OwnersValidatorIT extends LightweightPluginDaemonTest {
+public class OwnersValidatorIT extends FindOwners {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   @Rule public Watcher watcher = new Watcher(logger);
 
-  @Inject private ProjectOperations projectOperations;
-
-  protected Project.NameKey newProject(String name) {
-    return projectOperations
-        .newProject()
-        .parent(Project.nameKey("All-Projects"))
-        .name(name)
-        .create();
-  }
-
-  private void switchProject(Project.NameKey p) throws Exception {
-    project = p;
-    testRepo = cloneProject(project);
-  }
-
   private void addProjectFile(String project, String file, String content) throws Exception {
     switchProject(newProject(project));
     PushOneCommit.Result c = createChange("c", file, content);
@@ -338,6 +320,28 @@
     assertThat(validate(event, false, ENABLED_CONFIG)).containsExactlyElementsIn(expected);
   }
 
+  @Test
+  public void simpleIncludeACLTest() throws Exception {
+    // If the user cannot read an included file,
+    // the file is not seen and errors won't be detected.
+    // Use pA/pB, because addProjectFile cannot create the same project again.
+    addProjectFile("pA", "d2/owners", "wrong\nxyz\n");
+    addProjectFile("pB", "d2/owners", "x@g.com\nerr\ninclude ../d2/owners\n");
+    blockRead(project); // current project is pB; set it to not readable
+    ImmutableMap<String, String> files =
+        ImmutableMap.of(
+            "d1/" + OWNERS,
+            "include ../d2/owners\ninclude /d2/owners\n"
+                + "include pA:/d2/owners\ninclude pB:/d2/owners\n");
+    ImmutableSet<String> expected =
+        ImmutableSet.of(
+            "MSG: unchecked: d1/OWNERS:4: include pB:/d2/owners", // cannot read pB
+            "ERROR: syntax: d2/owners:1: wrong",
+            "ERROR: syntax: d2/owners:2: xyz");
+    CommitReceivedEvent event = makeCommitEvent("pA", "T", files);
+    assertThat(validate(event, false, ENABLED_CONFIG)).containsExactlyElementsIn(expected);
+  }
+
   private static PluginConfig createEnabledConfig() {
     PluginConfig c = new PluginConfig("", new Config());
     c.setBoolean(REJECT_ERROR_IN_OWNERS, true);
@@ -387,8 +391,9 @@
   private List<String> validate(CommitReceivedEvent event, boolean verbose, PluginConfig cfg)
       throws Exception {
     OwnersValidator validator =
-        new OwnersValidator("find-owners", pluginConfig, repoManager, new MockedEmails());
-    OwnersValidator.Checker checker = validator.new Checker(event, verbose);
+        new OwnersValidator(
+            "find-owners", permissionBackend, pluginConfig, repoManager, new MockedEmails());
+    OwnersValidator.Checker checker = validator.new Checker(event, verbose, permissionBackend);
     checker.check(OwnersValidator.getOwnersFileName(cfg));
     return transformMessages(checker.messages);
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
index fee0569..8a0481b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
@@ -18,6 +18,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,8 +41,8 @@
 
   private static Parser.Result testLine(String line) {
     Parser.Result result = new Parser.Result();
-    // Single line parser tests do not need repoManager.
-    Parser parser = new Parser(null, null, mockedProject(), "master", "OWNERS");
+    // Single line parser tests do not need a repository.
+    Parser parser = new Parser(mockedProject(), "master", "OWNERS");
     parser.parseLine(result, mockedTestDir(), line, 3);
     return result;
   }
@@ -273,32 +275,18 @@
 
   @Test
   public void getIncludeOrFileTest() {
-    String[] line =
-        new String[] {
-          "",
-          "wrong input",
-          "INCLUDE X",
-          "include //f2.txt # ",
-          "  include  P1/P2:  ../f1 # ",
-          "  file://f3 # ",
-          "file:  P1:f3",
-          "  per-file *.c,file.c = file:  /OWNERS  # ",
-          "per-file  *=file:P1/P2:  /O# ",
-        };
-    String[] expected =
-        new String[] {
-          "",
-          "",
-          "",
-          "include //f2.txt",
-          "include P1/P2:../f1",
-          "file://f3",
-          "file:P1:f3",
-          "file:/OWNERS",
-          "file:P1/P2:/O",
-        };
-    for (int i = 0; i < line.length; i++) {
-      assertThat(Parser.getIncludeOrFile(line[i])).isEqualTo(expected[i]);
+    Map<String, String> tests = new HashMap<>(); // map from input to expected result
+    tests.put("", "");
+    tests.put("wrong input", "");
+    tests.put("INCLUDE X", "");
+    tests.put("include //f2.txt # ", "include //f2.txt");
+    tests.put("  include  P1/P2:  ../f1 # ", "include P1/P2:../f1");
+    tests.put("  file://f3 # ", "file://f3");
+    tests.put("file:  P1:f3", "file:P1:f3");
+    tests.put("  per-file *.c,file.c = file:  /OWNERS  # ", "file:/OWNERS");
+    tests.put("per-file  *=file:P1/P2:  /O# ", "file:P1/P2:/O");
+    for (String line : tests.keySet()) {
+      assertThat(Parser.getIncludeOrFile(line)).isEqualTo(tests.get(line));
     }
   }