add capability to filter modules by metadata field

Bug: 36140955
Bug: 35360169
Test: added unit test
Change-Id: Iae6470c90f09e35bf9c6b74c3e7c75762d8dc865
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
index bba0c00..b5159d7 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
@@ -58,6 +58,7 @@
 import com.android.tradefed.util.AbiFormatter;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.TimeUtil;
 
@@ -270,6 +271,23 @@
                     + "actually carried out.")
     private Boolean mCollectTestsOnly = null;
 
+    @Option(name = "module-metadata-include-filter",
+            description = "Include modules for execution based on matching of metadata fields: "
+                    + "for any of the specified filter name and value, if a module has a metadata "
+                    + "field with the same name and value, it will be included. When both module "
+                    + "inclusion and exclusion rules are applied, inclusion rules will be "
+                    + "evaluated first. Using this together with test filter inclusion rules may "
+                    + "result in no tests to execute if the rules don't overlap.")
+    private MultiMap<String, String> mModuleMetadataIncludeFilter = new MultiMap<>();
+
+    @Option(name = "module-metadata-exclude-filter",
+            description = "Exclude modules for execution based on matching of metadata fields: "
+                    + "for any of the specified filter name and value, if a module has a metadata "
+                    + "field with the same name and value, it will be excluded. When both module "
+                    + "inclusion and exclusion rules are applied, inclusion rules will be "
+                    + "evaluated first.")
+    private MultiMap<String, String> mModuleMetadataExcludeFilter = new MultiMap<>();
+
     private int mTotalShards;
     private Integer mShardIndex = null;
     private IModuleRepo mModuleRepo;
@@ -358,7 +376,9 @@
                     // throw a {@link FileNotFoundException}
                     mModuleRepo.initialize(mTotalShards, mShardIndex, mBuildHelper.getTestsDir(),
                             getAbis(), mDeviceTokens, mTestArgs, mModuleArgs, mIncludeFilters,
-                            mExcludeFilters, mBuildHelper.getBuildInfo());
+                            mExcludeFilters,
+                            mModuleMetadataIncludeFilter, mModuleMetadataExcludeFilter,
+                            mBuildHelper.getBuildInfo());
 
                     // Add the entire list of modules to the CompatibilityBuildHelper for reporting
                     mBuildHelper.setModuleIds(mModuleRepo.getModuleIds());
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/IModuleRepo.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/IModuleRepo.java
index f75fbd1..b0c1bbed 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/IModuleRepo.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/IModuleRepo.java
@@ -17,6 +17,7 @@
 
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.util.MultiMap;
 
 import java.io.File;
 import java.util.LinkedList;
@@ -38,7 +39,10 @@
      */
     void initialize(int shards, Integer shardIndex, File testsDir, Set<IAbi> abis,
             List<String> deviceTokens, List<String> testArgs, List<String> moduleArgs,
-            Set<String> mIncludeFilters, Set<String> mExcludeFilters, IBuildInfo buildInfo);
+            Set<String> mIncludeFilters, Set<String> mExcludeFilters,
+            MultiMap<String, String> metadataIncludeFilters,
+            MultiMap<String, String> metadataExcludeFilters,
+            IBuildInfo buildInfo);
 
     /**
      * @return a {@link LinkedList} of all modules to run on the device referenced by the given
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java
index 82b8980..76dbf55 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleRepo.java
@@ -35,8 +35,11 @@
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.MultiMap;
 import com.android.tradefed.util.TimeUtil;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
@@ -165,7 +168,10 @@
     @Override
     public void initialize(int totalShards, Integer shardIndex, File testsDir, Set<IAbi> abis,
             List<String> deviceTokens, List<String> testArgs, List<String> moduleArgs,
-            Set<String> includeFilters, Set<String> excludeFilters, IBuildInfo buildInfo) {
+            Set<String> includeFilters, Set<String> excludeFilters,
+            MultiMap<String, String> metadataIncludeFilters,
+            MultiMap<String, String> metadataExcludeFilters,
+            IBuildInfo buildInfo) {
         CLog.d("Initializing ModuleRepo\nShards:%d\nTests Dir:%s\nABIs:%s\nDevice Tokens:%s\n" +
                 "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
                 totalShards, testsDir.getAbsolutePath(), abis, deviceTokens, testArgs, moduleArgs,
@@ -225,6 +231,12 @@
                     }
 
                     IConfiguration config = mConfigFactory.createConfigurationFromArgs(pathArg);
+                    if (!filterByConfigMetadata(config,
+                            metadataIncludeFilters, metadataExcludeFilters)) {
+                        // if the module config did not pass the metadata filters, it's excluded
+                        // from execution
+                        continue;
+                    }
                     Map<String, List<String>> args = new HashMap<>();
                     if (mModuleArgs.containsKey(name)) {
                         args.putAll(mModuleArgs.get(name));
@@ -370,6 +382,47 @@
         }
     }
 
+    @VisibleForTesting
+    protected boolean filterByConfigMetadata(IConfiguration config,
+            MultiMap<String, String> include, MultiMap<String, String> exclude) {
+        MultiMap<String, String> metadata = config.getConfigurationDescription().getAllMetaData();
+        boolean shouldInclude = false;
+        for (String key : include.keySet()) {
+            Set<String> filters = new HashSet<>(include.get(key));
+            if (metadata.containsKey(key)) {
+                filters.retainAll(metadata.get(key));
+                if (!filters.isEmpty()) {
+                    // inclusion filter is not empty and there's at least one matching inclusion
+                    // rule so there's no need to match other inclusion rules
+                    shouldInclude = true;
+                    break;
+                }
+            }
+        }
+        if (!include.isEmpty() && !shouldInclude) {
+            // if inclusion filter is not empty and we didn't find a match, the module will not be
+            // included
+            return false;
+        }
+        // Now evaluate exclusion rules, this ordering also means that exclusion rules may override
+        // inclusion rules: a config already matched for inclusion may still be excluded if matching
+        // rules exist
+        for (String key : exclude.keySet()) {
+            Set<String> filters = new HashSet<>(exclude.get(key));
+            if (metadata.containsKey(key)) {
+                filters.retainAll(metadata.get(key));
+                if (!filters.isEmpty()) {
+                    // we found at least one matching exclusion rules, so we are excluding this
+                    // this module
+                    return false;
+                }
+            }
+        }
+        // we've matched at least one inclusion rule (if there's any) AND we didn't match any of the
+        // exclusion rules (if there's any)
+        return true;
+    }
+
     private boolean shouldRunModule(String moduleId) {
         List<TestFilter> mdIncludes = getFilter(mIncludeFilters, moduleId);
         List<TestFilter> mdExcludes = getFilter(mExcludeFilters, moduleId);
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleRepoTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleRepoTest.java
index 23fc90c..8beb9a7 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleRepoTest.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleRepoTest.java
@@ -19,7 +19,9 @@
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.compatibility.common.tradefed.testtype.ModuleRepo.ConfigFilter;
 import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.targetprep.ITargetPreparer;
@@ -33,6 +35,7 @@
 import com.android.tradefed.testtype.ITestFilterReceiver;
 import com.android.tradefed.util.AbiUtils;
 import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.MultiMap;
 
 import junit.framework.TestCase;
 
@@ -77,6 +80,8 @@
     private static final List<String> MODULE_ARGS = new ArrayList<>();
     private static final Set<String> INCLUDES = new HashSet<>();
     private static final Set<String> EXCLUDES = new HashSet<>();
+    private static final MultiMap<String, String> METADATA_INCLUDES = new MultiMap<>();
+    private static final MultiMap<String, String> METADATA_EXCLUDES = new MultiMap<>();
     private static final Set<String> FILES = new HashSet<>();
     private static final String FILENAME = "%s.config";
     private static final String ROOT_DIR_ATTR = "ROOT_DIR";
@@ -185,7 +190,7 @@
 
     public void testInitialization() throws Exception {
         mRepo.initialize(3, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS, INCLUDES,
-                EXCLUDES, mMockBuildInfo);
+                EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of shards", 3, mRepo.getNumberOfShards());
         Map<String, Set<String>> deviceTokens = mRepo.getDeviceTokens();
@@ -200,7 +205,7 @@
 
     public void testGetModules() throws Exception {
         mRepo.initialize(1, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS, INCLUDES,
-                EXCLUDES, mMockBuildInfo);
+                EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of tokens", 2, mRepo.getTokenModules().size());
         assertEquals("Wrong number of tokens", 4, mRepo.getNonTokenModules().size());
@@ -211,7 +216,7 @@
      */
     public void testGetModulesSharded() throws Exception {
         mRepo.initialize(2, null, mTestsDir, ABIS, new ArrayList<String>(), TEST_ARGS, MODULE_ARGS,
-                INCLUDES, EXCLUDES, mMockBuildInfo);
+                INCLUDES, EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of tokens", 2, mRepo.getTokenModules().size());
         assertEquals("Wrong number of tokens", 4, mRepo.getNonTokenModules().size());
@@ -233,7 +238,7 @@
         Set<String> includes = new HashSet<>();
         includes.add(MODULE_NAME_C);
         mRepo.initialize(1, null, mTestsDir, ABIS, new ArrayList<String>(), TEST_ARGS, MODULE_ARGS,
-                includes, EXCLUDES, mMockBuildInfo);
+                includes, EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of tokens", 2, mRepo.getTokenModules().size());
         assertEquals("Wrong number of tokens", 0, mRepo.getNonTokenModules().size());
@@ -255,7 +260,7 @@
         tokens.add(String.format("%s:%s", SERIAL1, FOOBAR_TOKEN));
         tokens.add(String.format("%s:%s", SERIAL2, "foobar2"));
         mRepo.initialize(2, null, mTestsDir, ABIS, tokens, TEST_ARGS, MODULE_ARGS,
-                includes, EXCLUDES, mMockBuildInfo);
+                includes, EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of tokens", 4, mRepo.getTokenModules().size());
         assertEquals("Wrong number of tokens", 0, mRepo.getNonTokenModules().size());
@@ -278,7 +283,7 @@
     public void testGetModulesSharded_uneven() throws Exception {
         createConfig(mTestsDir, "FooModuleD", null);
         mRepo.initialize(4, null, mTestsDir, ABIS, new ArrayList<String>(), TEST_ARGS, MODULE_ARGS,
-                INCLUDES, EXCLUDES, mMockBuildInfo);
+                INCLUDES, EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
         assertEquals("Wrong number of tokens", 2, mRepo.getTokenModules().size());
         assertEquals("Wrong number of tokens", 6, mRepo.getNonTokenModules().size());
@@ -320,7 +325,8 @@
         excludeFilters.add(ID_A_32);
         excludeFilters.add(MODULE_NAME_B);
         mRepo.initialize(1, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS,
-                includeFilters, excludeFilters, mMockBuildInfo);
+                includeFilters, excludeFilters, METADATA_INCLUDES, METADATA_EXCLUDES,
+                mMockBuildInfo);
         List<IModuleDef> modules = mRepo.getModules(SERIAL1, 0);
         assertEquals("Incorrect number of modules", 1, modules.size());
         IModuleDef module = modules.get(0);
@@ -333,11 +339,11 @@
      */
     public void testInitialization_ExcludeModule_SkipLoadingConfig() {
         try {
-            Set<String> excludeFilters = new HashSet<String>() {{
-                    add(NON_EXISTS_MODULE_NAME);
-            }};
+            Set<String> excludeFilters = new HashSet<String>();
+            excludeFilters.add(NON_EXISTS_MODULE_NAME);
             mRepo.initialize(1, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS,
                     MODULE_ARGS, Collections.emptySet(), excludeFilters,
+                    METADATA_INCLUDES, METADATA_EXCLUDES,
                     mMockBuildInfo);
         } catch (Exception e) {
             fail("Initialization should not fail if non-existing module is excluded");
@@ -354,14 +360,15 @@
         excludeFilters.add(MODULE_NAME_B);
         excludeFilters.add(MODULE_NAME_C);
         mRepo.initialize(1, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS,
-                includeFilters, excludeFilters, mMockBuildInfo);
+                includeFilters, excludeFilters,
+                METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         List<IModuleDef> modules = mRepo.getModules(SERIAL1, 0);
         assertEquals("Incorrect number of modules", 0, modules.size());
     }
 
     public void testParsing() throws Exception {
         mRepo.initialize(1, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS, INCLUDES,
-                EXCLUDES, mMockBuildInfo);
+                EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         List<IModuleDef> modules = mRepo.getModules(SERIAL3, 0);
         Set<String> idSet = new HashSet<>();
         for (IModuleDef module : modules) {
@@ -396,7 +403,7 @@
         ArrayList<String> emptyList = new ArrayList<>();
 
         mRepo.initialize(3, 0, mTestsDir, abis, DEVICE_TOKENS, emptyList, emptyList, INCLUDES,
-                         EXCLUDES, mMockBuildInfo);
+                         EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
 
         List<IModuleDef> modules = new ArrayList<>();
         modules.addAll(mRepo.getNonTokenModules());
@@ -414,7 +421,7 @@
 
     public void testGetModuleIds() {
         mRepo.initialize(3, null, mTestsDir, ABIS, DEVICE_TOKENS, TEST_ARGS, MODULE_ARGS, INCLUDES,
-                EXCLUDES, mMockBuildInfo);
+                EXCLUDES, METADATA_INCLUDES, METADATA_EXCLUDES, mMockBuildInfo);
         assertTrue("Should be initialized", mRepo.isInitialized());
 
         assertArrayEquals(EXPECTED_MODULE_IDS, mRepo.getModuleIds());
@@ -506,4 +513,259 @@
         List<IModuleDef> res = mRepo.getShard(testList, 1, 2);
         assertNull(res);
     }
+
+    /**
+     * When there are no metadata based filters specified, config should be included
+     * @throws Exception
+     */
+    public void testMetadataFilter_emptyFilters() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        assertTrue("config not included when metadata filters are empty",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When inclusion filter is specified, config matching the filter is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchInclude() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo");
+        assertTrue("config not included with matching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When inclusion filter is specified, config not matching the filter is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_noMatchInclude_mismatchValue() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "bar");
+        assertFalse("config not excluded with mismatching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When inclusion filter is specified, config not matching the filter is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_noMatchInclude_mismatchKey() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("group", "bar");
+        assertFalse("config not excluded with mismatching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When exclusion filter is specified, config matching the filter is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchExclude() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("component", "foo");
+        assertFalse("config not excluded with matching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When exclusion filter is specified, config not matching the filter is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_noMatchExclude_mismatchKey() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("component", "bar");
+        assertTrue("config not included with mismatching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When exclusion filter is specified, config not matching the filter is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_noMatchExclude_mismatchValue() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("group", "bar");
+        assertTrue("config not included with mismatching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When inclusion filter is specified, config with one of the metadata field matching the filter
+     * is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchInclude_multipleMetadataField() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        metadata.put("component", "bar");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo");
+        assertTrue("config not included with matching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When exclusion filter is specified, config with one of the metadata field matching the filter
+     * is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchExclude_multipleMetadataField() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        metadata.put("component", "bar");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("component", "foo");
+        assertFalse("config not excluded with matching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When inclusion filters are specified, config with metadata field matching one of the filter
+     * is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchInclude_multipleFilters() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo");
+        includeFilter.put("component", "bar");
+        assertTrue("config not included with matching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When exclusion filters are specified, config with metadata field matching one of the filter
+     * is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchExclude_multipleFilters() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("component", "foo");
+        excludeFilter.put("component", "bar");
+        assertFalse("config not excluded with matching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When inclusion filters are specified, config with metadata field matching one of the filter
+     * is included
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchInclude_multipleMetadataAndFilters() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo1");
+        metadata.put("group", "bar1");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo1");
+        includeFilter.put("group", "bar2");
+        assertTrue("config not included with matching inclusion filter",
+                mRepo.filterByConfigMetadata(config, includeFilter, METADATA_EXCLUDES));
+    }
+
+    /**
+     * When exclusion filters are specified, config with metadata field matching one of the filter
+     * is excluded
+     * @throws Exception
+     */
+    public void testMetadataFilter_matchExclude_multipleMetadataAndFilters() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo1");
+        metadata.put("group", "bar1");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("component", "foo1");
+        excludeFilter.put("group", "bar2");
+        assertFalse("config not excluded with matching exclusion filter",
+                mRepo.filterByConfigMetadata(config, METADATA_INCLUDES, excludeFilter));
+    }
+
+    /**
+     * When inclusion and exclusion filters are both specified, config can pass through the filters
+     * as expected.
+     * @throws Exception
+     */
+    public void testMetadataFilter_includeAndExclude() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        metadata.put("group", "bar1");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo");
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("group", "bar2");
+        assertTrue("config not included with matching inclusion and mismatching exclusion filters",
+                mRepo.filterByConfigMetadata(config, includeFilter, excludeFilter));
+    }
+
+    /**
+     * When inclusion and exclusion filters are both specified, config be excluded as specified
+     * @throws Exception
+     */
+    public void testMetadataFilter_includeThenExclude() throws Exception {
+        IConfiguration config = new Configuration("foo", "bar");
+        ConfigurationDescriptor desc = config.getConfigurationDescription();
+        MultiMap<String, String> metadata = new MultiMap<>();
+        metadata.put("component", "foo");
+        metadata.put("group", "bar");
+        desc.setMetaData(metadata);
+        MultiMap<String, String> includeFilter = new MultiMap<>();
+        includeFilter.put("component", "foo");
+        MultiMap<String, String> excludeFilter = new MultiMap<>();
+        excludeFilter.put("group", "bar");
+        assertFalse("config not excluded with matching inclusion and exclusion filters",
+                mRepo.filterByConfigMetadata(config, includeFilter, excludeFilter));
+    }
 }