RuleBase: Read rules also from project config

So far, the rules defining the conditions and the actions affecting an
ITS issue were defined globally in <gerrit_site>/etc/its folder and all
the projects were subject to the same rules.

This can become a limitation as the handling of ITS issues can be very
different from one project to another, even if they use the same ITS.

Allow rules to be defined at the project level. As with the current
global rules, two files can be defined: 'actions.config' to define
general rules at the project level and 'actions-<its-name>.config' to
define rules specific to an ITS system. These files should be created in
the refs/meta/config branch and, similar with what happens now with the
global rules, the set of rules to be applied to an ITS issue is the sum
of the rules defined in the two files.

Once rules are defined at the project level, the global rules, i.e.,
those defined in <gerrit_site>/etc/its, no longer have effect in the
project; this means, the more specific rules take precedence.

As with other project defined configurations, inheritance is honored so
rules can be defined in a parent project. This inheritance, however, is
capped at the closest level, i.e., if a project defines at least one the
rules files, the presence of these files is not evaluated for any of the
project's parents.

Reading the project configuration to look up for rules every time an
issue is evaluated does not scale. Even if the ProjectLevelConfiguration
is already cached to avoid re-reading the repository, caching the rules
by project avoid re-parsing the configuration files and checking the
project inheritance for rules.

Change-Id: I2bb00a6c2687d41c9f4d8f73a0c0387ab3cb8834
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
index 530e45d..cfb6168 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/ItsHookModule.java
@@ -34,6 +34,7 @@
 import com.googlesource.gerrit.plugins.its.base.workflow.AddSoyComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.AddStandardComment;
 import com.googlesource.gerrit.plugins.its.base.workflow.Condition;
+import com.googlesource.gerrit.plugins.its.base.workflow.ItsRulesProjectCacheImpl;
 import com.googlesource.gerrit.plugins.its.base.workflow.LogEvent;
 import com.googlesource.gerrit.plugins.its.base.workflow.Rule;
 import java.nio.file.Path;
@@ -69,6 +70,7 @@
     factory(AddSoyComment.Factory.class);
     factory(AddStandardComment.Factory.class);
     factory(LogEvent.Factory.class);
+    install(ItsRulesProjectCacheImpl.module());
   }
 
   @Provides
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCache.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCache.java
new file mode 100644
index 0000000..265032b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCache.java
@@ -0,0 +1,22 @@
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import java.util.List;
+
+/** Cache of project defined ITS rules */
+public interface ItsRulesProjectCache {
+
+  /**
+   * Get the cached ITS rules for a project
+   *
+   * @param projectName name of the project.
+   * @return the cached rules; an empty list if no such project exists or projectName is null.
+   */
+  List<Rule> get(String projectName);
+
+  /**
+   * Invalidate the cached rules for the given project.
+   *
+   * @param projectName project for which the rules are being evicted
+   */
+  void evict(String projectName);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java
new file mode 100644
index 0000000..7e37397
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheImpl.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2018 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.plugins.its.base.workflow;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.its.base.GlobalRulesFileName;
+import com.googlesource.gerrit.plugins.its.base.PluginRulesFileName;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ItsRulesProjectCacheImpl implements ItsRulesProjectCache {
+  private static final Logger log = LoggerFactory.getLogger(ItsRulesProjectCacheImpl.class);
+  private static final String CACHE_NAME = "its_rules_project";
+
+  private final LoadingCache<String, List<Rule>> cache;
+
+  @Inject
+  ItsRulesProjectCacheImpl(@Named(CACHE_NAME) LoadingCache<String, List<Rule>> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public List<Rule> get(String projectName) {
+    try {
+      return cache.get(projectName);
+    } catch (ExecutionException e) {
+      log.warn("Cannot get project specific rules for project {}", projectName, e);
+      return ImmutableList.of();
+    }
+  }
+
+  @Override
+  public void evict(String projectName) {
+    cache.invalidate(projectName);
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, String.class, new TypeLiteral<List<Rule>>() {}).loader(Loader.class);
+
+        bind(ItsRulesProjectCacheImpl.class);
+        bind(ItsRulesProjectCache.class).to(ItsRulesProjectCacheImpl.class);
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+            .to(ItsRulesProjectCacheRefresher.class);
+      }
+    };
+  }
+
+  static class Loader extends CacheLoader<String, List<Rule>> {
+    private final String globalRulesFileName;
+    private final String pluginRulesFileName;
+    private final ProjectCache projectCache;
+    private final RulesConfigReader rulesConfigReader;
+
+    @Inject
+    Loader(
+        @GlobalRulesFileName String globalRulesFileName,
+        @PluginRulesFileName String pluginRulesFileName,
+        ProjectCache projectCache,
+        RulesConfigReader rulesConfigReader) {
+      this.globalRulesFileName = globalRulesFileName;
+      this.pluginRulesFileName = pluginRulesFileName;
+      this.projectCache = projectCache;
+      this.rulesConfigReader = rulesConfigReader;
+    }
+
+    @Override
+    public List<Rule> load(String projectName) throws IOException {
+      ProjectState project = projectCache.checkedGet(new Project.NameKey(projectName));
+      List<Rule> projectRules = readRulesFrom(project);
+      if (projectRules.isEmpty()) {
+        for (ProjectState parent : project.parents()) {
+          projectRules = readRulesFrom(parent);
+          if (!projectRules.isEmpty()) {
+            break;
+          }
+        }
+      }
+      return projectRules;
+    }
+
+    private List<Rule> readRulesFrom(ProjectState project) {
+      Config general = project.getConfig(globalRulesFileName).get();
+      Config pluginSpecific = project.getConfig(pluginRulesFileName).get();
+      return new ImmutableList.Builder<Rule>()
+          .addAll(rulesConfigReader.getRulesFromConfig(general))
+          .addAll(rulesConfigReader.getRulesFromConfig(pluginSpecific))
+          .build();
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheRefresher.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheRefresher.java
new file mode 100644
index 0000000..cd119a4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheRefresher.java
@@ -0,0 +1,22 @@
+package com.googlesource.gerrit.plugins.its.base.workflow;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.inject.Inject;
+
+public class ItsRulesProjectCacheRefresher implements GitReferenceUpdatedListener {
+
+  private final ItsRulesProjectCache itsRuleProjectCache;
+
+  @Inject
+  ItsRulesProjectCacheRefresher(ItsRulesProjectCache itsRuleProjectCache) {
+    this.itsRuleProjectCache = itsRuleProjectCache;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    if (event.getRefName().equals(RefNames.REFS_CONFIG)) {
+      itsRuleProjectCache.evict(event.getProjectName());
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
index fffb7a0..b6e4ad6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBase.java
@@ -15,7 +15,6 @@
 package com.googlesource.gerrit.plugins.its.base.workflow;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.its.base.GlobalRulesFileName;
 import com.googlesource.gerrit.plugins.its.base.ItsPath;
@@ -23,6 +22,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
@@ -38,6 +38,7 @@
 
   private final File globalRuleFile;
   private final File itsSpecificRuleFile;
+  private final ItsRulesProjectCache rulesProjectCache;
 
   private Collection<Rule> rules;
 
@@ -50,9 +51,11 @@
       @ItsPath Path itsPath,
       @GlobalRulesFileName String globalRulesFileName,
       @PluginRulesFileName String pluginRulesFileName,
+      ItsRulesProjectCache rulesProjectCache,
       RulesConfigReader rulesConfigReader) {
     this.globalRuleFile = itsPath.resolve(globalRulesFileName).toFile();
     this.itsSpecificRuleFile = itsPath.resolve(pluginRulesFileName).toFile();
+    this.rulesProjectCache = rulesProjectCache;
     this.rules =
         new ImmutableList.Builder<Rule>()
             .addAll(getRulesFromFile(rulesConfigReader, globalRuleFile))
@@ -91,10 +94,22 @@
    * @return Requests for the actions that should be fired.
    */
   public Collection<ActionRequest> actionRequestsFor(Map<String, String> properties) {
-    Collection<ActionRequest> ret = Lists.newLinkedList();
-    for (Rule rule : rules) {
-      ret.addAll(rule.actionRequestsFor(properties));
+    String projectName = properties.get("project");
+    Collection<Rule> fromProjectConfig = rulesProjectCache.get(projectName);
+    Collection<Rule> rulesToAdd = !fromProjectConfig.isEmpty() ? fromProjectConfig : rules;
+    if (rulesToAdd.isEmpty() && !globalRuleFile.exists() && !itsSpecificRuleFile.exists()) {
+      log.warn(
+          "Neither global rule file {} nor Its specific rule file {} exist and no rules are "
+              + "configured for project {}. Please configure rules.",
+          globalRuleFile,
+          itsSpecificRuleFile,
+          projectName);
+      return Collections.emptyList();
     }
-    return ret;
+    Collection<ActionRequest> actions = new ArrayList<>();
+    for (Rule rule : rulesToAdd) {
+      actions.addAll(rule.actionRequestsFor(properties));
+    }
+    return actions;
   }
 }
diff --git a/src/main/resources/Documentation/config-rulebase-common.md b/src/main/resources/Documentation/config-rulebase-common.md
index eacc1ef..128dac5 100644
--- a/src/main/resources/Documentation/config-rulebase-common.md
+++ b/src/main/resources/Documentation/config-rulebase-common.md
@@ -18,16 +18,12 @@
 ‘Resolved’”) on the ITS.
 
 Actions on the ITS and conditions for the action to take place are
-configured through the rule bases in `etc/its/actions.config` (for global rules
-to be picked up by all configured ITS plugins) and
-`etc/its/actions-@PLUGIN@.config` (for rules to be picked up only by @PLUGIN@)
-in the site directory. A rule base is a git config file, and may contain an
-arbitrary number of rules. Each rule can have an arbitrary number of conditions
-and actions. A rule fires all associated actions, once all of its conditions are
-met.
+configured through rule bases.  A rule base is a git config file and
+may contain an arbitrary number of rules. Each rule can have an
+arbitrary number of conditions and actions. A rule fires all associated
+actions, once all of its conditions are met.
 
-A simple `etc/its/actions.config` (or
-`etc/its/actions-@PLUGIN@.config`) may look like
+A simple rule bases file may look like
 
 ```
 [rule "rule1"]
@@ -47,9 +43,92 @@
 Goodness! Someone gave a negative code review in Gerrit on an
 associated change.” to each such issue.
 
-The order of rules in `etc/its/actions.config` need not be
-respected. So in the above example, do not rely on `rule1` being
-evaluated before `rule2`.
+The order of rules in a rule base file need not be respected. So in the
+above example, do not rely on `rule1` being evaluated before `rule2`.
+
+[rules-scope]: #rules-scope
+<a name="rules">Rule bases scope</a>
+-------------------------
+
+Rule bases can be defined in two scopes:
+
+* Global
+* Plugin specific
+
+Global rules are picked up by all configured ITS plugins and they are
+defined in a rule base file named `actions.config`.
+
+Plugin specific rules are picked up only by @PLUGIN@ plugin and they
+are defined in a rule base file named `actions-@PLUGIN@.config`.
+
+A second aspect of rules bases scope is what projects they apply to;
+in this case the rules can be:
+
+* Generic
+* Project specific
+
+The generic scope refers to rules that apply to all projects that
+enable ITS integration on the gerrit site; they are defined on rule
+base files located inside the `gerrit_site/etc/its/` folder.
+
+Project-specific rules are defined on rule base files located on the
+`refs/meta/config` branch of a project and they apply exclusively to
+the project that defines them (or that inherits them) and, possibly,
+to child projects (see further explanations about rules inheritance
+below). Project specific rules take precedence over generic rules,
+i.e, when they are defined, generic rules do not apply to the project.
+
+Thus, to define global generic rules, i.e., rules that are picked up
+by all the ITS plugins and that apply to all the projects that enable
+any ITS integration, they should be defined in the
+`gerrit_site/etc/its/actions.config` rule base file.
+
+Rules defined in the `gerrit_site/etc/its/actions-@PLUGIN@.config` rule
+base file have generic but plugin specific scope, i.e., they apply to
+all projects on the gerrit site that enable integration with @PLUGIN@.
+
+On the other hand, if the rule base file `actions.config` is created
+on the `refs/meta/config` branch of project 'P', the rules defined
+on this file will have global but project specific scope, i.e, they
+apply to all the ITS integrations defined for this project. Thus, if
+project 'P' integrates with ITS system 'x' and with ITS system 'y',
+the rules are applied to these two ITS integrations.
+
+Contrarily, rules defined on the rule base file `actions-@PLUGIN@.config`
+created on the `refs/meta/config` branch of project 'P' have project
+and plugin specific scope, i.e., they apply only to the @PLUGIN@
+integration defined for project 'P'.
+
+Finally, is important to notice that if global and plugin specific rules
+are defined, the final set of rules applied is the merge of them and this
+is true either if they are defined in generic or project specific scope.
+
+[rules-inheritance]: #rules-inheritance
+### <a name="rules-inheritance">Rules inheritance</a>
+
+For project specific rules, i.e., those defined on the `refs/meta/config`
+branch of a project, inheritance is honored, similar to what is done in
+other cases of project configurations.
+
+Thus, if project 'P' defines project specific rules, these are applied
+to children projects of project 'P' that enable an ITS integration.
+
+This inheritance, however, is capped at the closest level, which means
+that if a project defines at least one of the rule bases files,
+(`actions.config` or `actions-@PLUGIN@.config`), the presence of these
+files is not evaluated for any of the project's parents.
+
+Thence, and continuing the example, if project 'Q' is a child of
+project 'P' and project 'Q' enables ITS integration, rules defined in
+project 'P' apply to project 'Q'. If however, project 'Q' defines its
+own set of rules, either on file `actions.config` or
+`actions-@PLUGIN@.config` (or both), the rules defined by project 'P'
+no longer apply to project 'Q' , i.e., rules defined in project 'Q'
+override and replace any rules defined on any parent project.
+
+The same applies to the rules defined at the site level: if any project
+defines their own rule base files, the global ones defined in the
+`gerrit_site/etc/its/` folder do not apply to this project.
 
 [rules]: #rules
 <a name="rules">Rules</a>
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java
new file mode 100644
index 0000000..3b33b41
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/ItsRulesProjectCacheTest.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2018 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.plugins.its.base.workflow;
+
+import static com.googlesource.gerrit.plugins.its.base.workflow.RulesConfigReader.ACTION_KEY;
+import static com.googlesource.gerrit.plugins.its.base.workflow.RulesConfigReader.RULE_SECTION;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectLevelConfig;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.googlesource.gerrit.plugins.its.base.GlobalRulesFileName;
+import com.googlesource.gerrit.plugins.its.base.PluginRulesFileName;
+import com.googlesource.gerrit.plugins.its.base.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.its.base.workflow.RuleBaseTest.RuleBaseKind;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+
+public class ItsRulesProjectCacheTest extends LoggingMockingTestCase {
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      rulesConfigReader = createMock(RulesConfigReader.class);
+      bind(RulesConfigReader.class).toInstance(rulesConfigReader);
+
+      projectCache = createMock(ProjectCache.class);
+      bind(ProjectCache.class).toInstance(projectCache);
+
+      bind(String.class)
+          .annotatedWith(GlobalRulesFileName.class)
+          .toInstance(RuleBaseKind.GLOBAL.fileName);
+      bind(String.class)
+          .annotatedWith(PluginRulesFileName.class)
+          .toInstance(RuleBaseKind.ITS.fileName);
+    }
+  }
+
+  private static final String ACTION_1 = "action1";
+  private static final String CONDITION_KEY = "condition";
+  private static final String RULE_1 = "rule1";
+  private static final String TEST_PROJECT = "testProject";
+  private static final String VALUE_1 = "value1";
+
+  private Injector injector;
+  private ProjectCache projectCache;
+  private RulesConfigReader rulesConfigReader;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  public void testProjectConfigIsLoaded() throws IOException {
+    Rule rule1 = new Rule(RULE_1);
+    ActionRequest action1 = new ActionRequest(ACTION_1);
+    Condition condition1 = new Condition(CONDITION_KEY, VALUE_1);
+    rule1.addActionRequest(action1);
+    rule1.addCondition(condition1);
+
+    ProjectState projectState = createMock(ProjectState.class);
+    ProjectLevelConfig projectLevelConfigGlobal = createMock(ProjectLevelConfig.class);
+    Config projectGlobalCfg = new Config();
+    projectGlobalCfg.setString(RULE_SECTION, RULE_1, CONDITION_KEY, VALUE_1);
+    projectGlobalCfg.setString(RULE_SECTION, RULE_1, ACTION_KEY, ACTION_1);
+    expect(projectLevelConfigGlobal.get()).andReturn(projectGlobalCfg);
+    expect(projectState.getConfig(RuleBaseKind.GLOBAL.fileName))
+        .andReturn(projectLevelConfigGlobal);
+    ProjectLevelConfig projectLevelConfigPlugin = createMock(ProjectLevelConfig.class);
+    expect(projectLevelConfigPlugin.get()).andReturn(new Config());
+    expect(projectState.getConfig(RuleBaseKind.ITS.fileName)).andReturn(projectLevelConfigPlugin);
+    expect(projectCache.checkedGet(new Project.NameKey(TEST_PROJECT))).andReturn(projectState);
+    expect(rulesConfigReader.getRulesFromConfig(isA(Config.class)))
+        .andReturn(ImmutableList.of(rule1))
+        .andReturn(ImmutableList.of());
+    replayMocks();
+
+    ItsRulesProjectCacheImpl.Loader loader =
+        injector.getInstance(ItsRulesProjectCacheImpl.Loader.class);
+    Collection<Rule> actual = loader.load(TEST_PROJECT);
+    List<Rule> expected = ImmutableList.of(rule1);
+
+    assertEquals("Rules do not match", expected, actual);
+    assertTrue(actual.contains(rule1));
+  }
+
+  public void testParentProjectConfigIsLoaded() throws IOException {
+    Rule rule1 = new Rule(RULE_1);
+    ActionRequest action1 = new ActionRequest(ACTION_1);
+    Condition condition1 = new Condition(CONDITION_KEY, VALUE_1);
+    rule1.addActionRequest(action1);
+    rule1.addCondition(condition1);
+
+    ProjectState projectState = createMock(ProjectState.class);
+    ProjectLevelConfig projectLevelConfigGlobal = createMock(ProjectLevelConfig.class);
+    expect(projectLevelConfigGlobal.get()).andReturn(new Config());
+    expect(projectState.getConfig(RuleBaseKind.GLOBAL.fileName))
+        .andReturn(projectLevelConfigGlobal);
+    ProjectLevelConfig projectLevelConfigPlugin = createMock(ProjectLevelConfig.class);
+    expect(projectLevelConfigPlugin.get()).andReturn(new Config());
+    expect(projectState.getConfig(RuleBaseKind.ITS.fileName)).andReturn(projectLevelConfigPlugin);
+
+    ProjectState parentProjectState = createMock(ProjectState.class);
+    ProjectLevelConfig parentProjectConfigGlobal = createMock(ProjectLevelConfig.class);
+    Config parentGlobalCfg = new Config();
+    parentGlobalCfg.setString(RULE_SECTION, RULE_1, CONDITION_KEY, VALUE_1);
+    parentGlobalCfg.setString(RULE_SECTION, RULE_1, ACTION_KEY, ACTION_1);
+    expect(parentProjectConfigGlobal.get()).andReturn(parentGlobalCfg);
+    expect(parentProjectState.getConfig(RuleBaseKind.GLOBAL.fileName))
+        .andReturn(parentProjectConfigGlobal);
+    ProjectLevelConfig parentProjectConfigPlugin = createMock(ProjectLevelConfig.class);
+    expect(parentProjectConfigPlugin.get()).andReturn(new Config());
+    expect(parentProjectState.getConfig(RuleBaseKind.ITS.fileName))
+        .andReturn(parentProjectConfigPlugin);
+    expect(projectState.parents()).andReturn(FluentIterable.of(parentProjectState));
+    expect(projectCache.checkedGet(new Project.NameKey(TEST_PROJECT))).andReturn(projectState);
+
+    expect(rulesConfigReader.getRulesFromConfig(isA(Config.class)))
+        .andReturn(ImmutableList.of())
+        .andReturn(ImmutableList.of())
+        .andReturn(ImmutableList.of(rule1))
+        .andReturn(ImmutableList.of());
+
+    replayMocks();
+
+    ItsRulesProjectCacheImpl.Loader loader =
+        injector.getInstance(ItsRulesProjectCacheImpl.Loader.class);
+    Collection<Rule> actual = loader.load(TEST_PROJECT);
+    List<Rule> expected = ImmutableList.of(rule1);
+
+    assertEquals("Rules do not match", expected, actual);
+    assertTrue(actual.contains(rule1));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
index 367d2b3..305e8d1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/its/base/workflow/RuleBaseTest.java
@@ -11,6 +11,7 @@
 // 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.plugins.its.base.workflow;
 
 import static org.easymock.EasyMock.expect;
@@ -39,15 +40,18 @@
 import org.eclipse.jgit.util.FileUtils;
 
 public class RuleBaseTest extends LoggingMockingTestCase {
+  private static final String PROJECT_KEY = "project";
+  private static final String TEST_PROJECT = "testProject";
 
   private Injector injector;
 
   private Path itsPath;
   private RulesConfigReader rulesConfigReader;
+  private ItsRulesProjectCache rulesProjectCache;
 
   private boolean cleanupSitePath;
 
-  private enum RuleBaseKind {
+  public enum RuleBaseKind {
     GLOBAL("actions"),
     ITS("actions-ItsTestName");
 
@@ -65,7 +69,7 @@
     Rule rule1 = createMock(Rule.class);
     ActionRequest actionRequest1 = createMock(ActionRequest.class);
 
-    Map<String, String> properties = ImmutableMap.of();
+    Map<String, String> properties = ImmutableMap.of(PROJECT_KEY, TEST_PROJECT);
 
     List<ActionRequest> rule1Match = Lists.newArrayListWithCapacity(1);
     rule1Match.add(actionRequest1);
@@ -75,6 +79,8 @@
         .andReturn(ImmutableList.of(rule1))
         .once();
 
+    expect(rulesProjectCache.get(TEST_PROJECT)).andReturn(ImmutableList.of());
+
     replayMocks();
 
     RuleBase ruleBase = createRuleBase();
@@ -102,7 +108,7 @@
     Rule rule2 = createMock(Rule.class);
     ActionRequest actionRequest3 = createMock(ActionRequest.class);
 
-    Map<String, String> properties = ImmutableMap.of();
+    Map<String, String> properties = ImmutableMap.of(PROJECT_KEY, TEST_PROJECT);
 
     List<ActionRequest> rule1Match = ImmutableList.of(actionRequest1, actionRequest2);
     expect(rule1.actionRequestsFor(properties)).andReturn(rule1Match).anyTimes();
@@ -110,6 +116,8 @@
     List<ActionRequest> rule2Match = ImmutableList.of(actionRequest3);
     expect(rule2.actionRequestsFor(properties)).andReturn(rule2Match).anyTimes();
 
+    expect(rulesProjectCache.get(TEST_PROJECT)).andReturn(ImmutableList.of());
+
     expect(rulesConfigReader.getRulesFromConfig(isA(Config.class)))
         .andReturn(ImmutableList.of(rule1, rule2))
         .andReturn(ImmutableList.of())
@@ -130,7 +138,7 @@
 
     injectRuleBase("[rule \"rule3\"]\n\taction = action3", RuleBaseKind.ITS);
 
-    Map<String, String> properties = ImmutableMap.of();
+    Map<String, String> properties = ImmutableMap.of(PROJECT_KEY, TEST_PROJECT);
 
     Rule rule2 = createMock(Rule.class);
     ActionRequest actionRequest2 = createMock(ActionRequest.class);
@@ -144,6 +152,8 @@
     List<ActionRequest> rule3Match = ImmutableList.of(actionRequest3);
     expect(rule3.actionRequestsFor(properties)).andReturn(rule3Match);
 
+    expect(rulesProjectCache.get(TEST_PROJECT)).andReturn(ImmutableList.of());
+
     expect(rulesConfigReader.getRulesFromConfig(isA(Config.class)))
         .andReturn(ImmutableList.of(rule2, rule3))
         .andReturn(ImmutableList.of())
@@ -160,6 +170,27 @@
     assertEquals("Matched actionRequests do not match", expected, actual);
   }
 
+  public void testProjectConfigIsLoaded() {
+    Rule rule1 = createMock(Rule.class);
+    ActionRequest actionRequest1 = createMock(ActionRequest.class);
+
+    Map<String, String> properties = ImmutableMap.of(PROJECT_KEY, TEST_PROJECT);
+
+    List<ActionRequest> rule1Match = ImmutableList.of(actionRequest1);
+    expect(rule1.actionRequestsFor(properties)).andReturn(rule1Match);
+
+    expect(rulesProjectCache.get(TEST_PROJECT)).andReturn(ImmutableList.of(rule1));
+
+    replayMocks();
+
+    RuleBase ruleBase = createRuleBase();
+    Collection<ActionRequest> actual = ruleBase.actionRequestsFor(properties);
+
+    List<ActionRequest> expected = ImmutableList.of(actionRequest1);
+
+    assertEquals("Matched actionRequests do not match", expected, actual);
+  }
+
   private RuleBase createRuleBase() {
     return injector.getInstance(RuleBase.class);
   }
@@ -210,6 +241,9 @@
       rulesConfigReader = createMock(RulesConfigReader.class);
       bind(RulesConfigReader.class).toInstance(rulesConfigReader);
 
+      rulesProjectCache = createMock(ItsRulesProjectCache.class);
+      bind(ItsRulesProjectCache.class).toInstance(rulesProjectCache);
+
       bind(String.class)
           .annotatedWith(GlobalRulesFileName.class)
           .toInstance(RuleBaseKind.GLOBAL.fileName);