/*
 * Copyright (C) 2010 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.android.tradefed.config;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import static java.util.Map.entry;

import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.build.IDeviceBuildProvider;
import com.android.tradefed.build.LocalDeviceBuildProvider;
import com.android.tradefed.config.ConfigurationDef.ConfigObjectDef;
import com.android.tradefed.config.ConfigurationFactory.ConfigId;
import com.android.tradefed.config.remote.IRemoteFileResolver.ResolvedFile;
import com.android.tradefed.log.ILeveledLogOutput;
import com.android.tradefed.log.Log.LogLevel;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.targetprep.DeviceWiper;
import com.android.tradefed.targetprep.ILabPreparer;
import com.android.tradefed.targetprep.StubTargetPreparer;
import com.android.tradefed.targetprep.multi.StubMultiTargetPreparer;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.StreamUtil;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/** Unit tests for {@link ConfigurationFactory} */
@RunWith(JUnit4.class)
public class ConfigurationFactoryTest {

    private ConfigurationFactory mFactory;
    // Create a real instance for tests that checks the content of our configs. making it static to
    // reduce the runtime of reloading the config thanks to the caching of configurations.
    private static ConfigurationFactory mRealFactory = new ConfigurationFactory();

    /** the test config name that is built into this jar */
    private static final String TEST_CONFIG = "test-config";
    private static final String GLOBAL_TEST_CONFIG = "global-config";
    private static final String INCLUDE_CONFIG = "include-config";

    private static final List<String> JAR_TO_CHECK = new ArrayList<>();

    static {
        JAR_TO_CHECK.add("tradefed-contrib.jar");
        JAR_TO_CHECK.add("google-tradefed-contrib.jar");
    }

    @Before
    public void setUp() throws Exception {

        mFactory =
                new ConfigurationFactory() {
                    @Override
                    protected String getConfigPrefix() {
                        return "testconfigs/";
                    }
                };
    }

    /** Initial test to ensure all config names on classpath are loadable */
    @Test
    public void testLoadAllConfigs() throws Exception {
        ConfigurationFactory spyFactory = Mockito.spy(mRealFactory);
        Mockito.doReturn(new HashSet<String>()).when(spyFactory).getConfigNamesFromTestCases(null);

        // we dry-run the templates otherwise it will always fail.
        spyFactory.loadAllConfigs(false);
        assertTrue(spyFactory.getMapConfig().size() > 0);
        Mockito.verify(spyFactory, Mockito.times(1)).getConfigNamesFromTestCases(null);

        // Verify that there is no cross-referencing between core and contrib:
        // core configs should not use any contrib object.
        Map<String, String> configAndJar = spyFactory.getConfigSetFromClasspathFromJar(null);
        Map<String, List<String>> classInContribJars = getClassInContribJar();

        List<String> exceptionConfigJar = new ArrayList<>();
        for (Entry<ConfigId, ConfigurationDef> entry : spyFactory.getMapConfig().entrySet()) {
            String configName = entry.getKey().name;
            String jarName = configAndJar.get(configName);
            ConfigurationDef cDef = entry.getValue();
            if (JAR_TO_CHECK.contains(jarName)) {
                continue;
            }

            for (List<ConfigObjectDef> cObjects : cDef.getObjectClassMap().values()) {
                for (ConfigObjectDef o : cObjects) {
                    for (String contribJar : classInContribJars.keySet()) {
                        if (classInContribJars.get(contribJar).contains(o.mClassName)) {
                            exceptionConfigJar.add(
                                    String.format(
                                            "%s is a core-configuration (%s). It shouldn't contain"
                                                    + " a contrib objects '%s' from %s.",
                                            configName, jarName, o.mClassName, contribJar));
                        }
                    }
                }
            }
        }
        if (!exceptionConfigJar.isEmpty()) {
            fail(
                    String.format(
                            "Found some dependencies of core on contrib:\n%s",
                            String.join("\n", exceptionConfigJar)));
        }
    }

    /** Initial test to ensure all configs on classpath can be fully loaded and parsed */
    @Test
    public void testLoadAndPrintAllConfigs() throws ConfigurationException {
        ConfigurationFactory spyFactory = Mockito.spy(mRealFactory);
        Mockito.doReturn(new HashSet<String>()).when(spyFactory).getConfigNamesFromTestCases(null);

        // Printing the help involves more checks since it tries to resolve the config objects.
        spyFactory.loadAndPrintAllConfigs();
        assertTrue(spyFactory.getMapConfig().size() > 0);
        Mockito.verify(spyFactory, Mockito.times(1)).getConfigNamesFromTestCases(null);
    }

    /** Test that a config xml defined in this test jar can be read as a built-in */
    @Test
    public void testGetConfiguration_extension() throws ConfigurationException {
        assertConfigValid(TEST_CONFIG);
    }

    private Map<String, String> buildMap(String... args) {
        if ((args.length % 2) != 0) {
            throw new IllegalArgumentException(String.format(
                "Expected an even number of args; got %d", args.length));
        }

        final Map<String, String> map = new HashMap<String, String>(args.length / 2);
        for (int i = 0; i < args.length; i += 2) {
            map.put(args[i], args[i + 1]);
        }

        return map;
    }

    /** Make sure that ConfigId behaves in the right way to serve as a hash key */
    @Test
    public void testConfigId_equals() {
        final ConfigId config1a = new ConfigId("one");
        final ConfigId config1b = new ConfigId("one");
        final ConfigId config2 = new ConfigId("two");
        final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
        final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
        final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));

        assertEquals(config1a, config1b);
        assertEquals(config3a, config3b);

        // Check for false equivalences, and don't depend on #equals being commutative
        assertFalse(config1a.equals(config2));
        assertFalse(config1a.equals(config3a));
        assertFalse(config1a.equals(config4));

        assertFalse(config2.equals(config1a));
        assertFalse(config2.equals(config3a));
        assertFalse(config2.equals(config4));

        assertFalse(config3a.equals(config1a));
        assertFalse(config3a.equals(config2));
        assertFalse(config3a.equals(config4));

        assertFalse(config4.equals(config1a));
        assertFalse(config4.equals(config2));
        assertFalse(config4.equals(config3a));
    }

    /** Make sure that ConfigId behaves in the right way to serve as a hash key */
    @Test
    public void testConfigId_hashKey() {
        final Map<ConfigId, String> map = new HashMap<>();
        final ConfigId config1a = new ConfigId("one");
        final ConfigId config1b = new ConfigId("one");
        final ConfigId config2 = new ConfigId("two");
        final ConfigId config3a = new ConfigId("one", buildMap("target", "foo"));
        final ConfigId config3b = new ConfigId("one", buildMap("target", "foo"));
        final ConfigId config4 = new ConfigId("two", buildMap("target", "bar"));

        // Make sure that keys config1a and config1b behave identically
        map.put(config1a, "1a");
        assertEquals("1a", map.get(config1a));
        assertEquals("1a", map.get(config1b));

        map.put(config1b, "1b");
        assertEquals("1b", map.get(config1a));
        assertEquals("1b", map.get(config1b));

        assertFalse(map.containsKey(config2));
        assertFalse(map.containsKey(config3a));
        assertFalse(map.containsKey(config4));

        // Make sure that keys config3a and config3b behave identically
        map.put(config3a, "3a");
        assertEquals("3a", map.get(config3a));
        assertEquals("3a", map.get(config3b));

        map.put(config3b, "3b");
        assertEquals("3b", map.get(config3a));
        assertEquals("3b", map.get(config3b));

        assertEquals(2, map.size());
        assertFalse(map.containsKey(config2));
        assertFalse(map.containsKey(config4));

        // It's unlikely for these to fail if the above tests all passed, but just fill everything
        // out for completeness
        map.put(config2, "2");
        map.put(config4, "4");

        assertEquals(4, map.size());
        assertEquals("1b", map.get(config1a));
        assertEquals("1b", map.get(config1b));
        assertEquals("2", map.get(config2));
        assertEquals("3b", map.get(config3a));
        assertEquals("3b", map.get(config3b));
        assertEquals("4", map.get(config4));
    }

    /** Test specifying a config xml by file path */
    @Test
    public void testGetConfiguration_xmlpath() throws ConfigurationException, IOException {
        // extract the test-config.xml into a tmp file
        InputStream configStream = getClass().getResourceAsStream(
                String.format("/testconfigs/%s.xml", TEST_CONFIG));
        File tmpFile = FileUtil.createTempFile(TEST_CONFIG, ".xml");
        try {
            FileUtil.writeToFile(configStream, tmpFile);
            assertConfigValid(tmpFile.getAbsolutePath());
            // check reading it again - should grab the cached version
            assertConfigValid(tmpFile.getAbsolutePath());
        } finally {
            FileUtil.deleteFile(tmpFile);
        }
    }

    /** Test that a config xml defined in this test jar can be read as a built-in */
    @Test
    public void testGetGlobalConfiguration_extension() throws ConfigurationException {
        assertGlobalConfigValid(GLOBAL_TEST_CONFIG);
    }

    /** Test specifying a config xml by file path */
    @Test
    public void testGetGlobalConfiguration_xmlpath() throws ConfigurationException, IOException {
        // extract the test-config.xml into a tmp file
        InputStream configStream = getClass().getResourceAsStream(
                String.format("/testconfigs/%s.xml", GLOBAL_TEST_CONFIG));
        File tmpFile = FileUtil.createTempFile(GLOBAL_TEST_CONFIG, ".xml");
        try {
            FileUtil.writeToFile(configStream, tmpFile);
            assertGlobalConfigValid(tmpFile.getAbsolutePath());
            // check reading it again - should grab the cached version
            assertGlobalConfigValid(tmpFile.getAbsolutePath());
        } finally {
            FileUtil.deleteFile(tmpFile);
        }
    }

    /**
     * Checks all config attributes are non-null
     */
    private void assertConfigValid(String name) throws ConfigurationException {
        IConfiguration config = mFactory.createConfigurationFromArgs(new String[] {name});
        assertNotNull(config);
    }

    /**
     * Checks all config attributes are non-null
     */
    private void assertGlobalConfigValid(String name) throws ConfigurationException {
        List<String> unprocessed = new ArrayList<String>();
        IGlobalConfiguration config =
                mFactory.createGlobalConfigurationFromArgs(new String[] {name}, unprocessed);
        assertNotNull(config);
        assertNotNull(config.getDeviceMonitors());
        assertNotNull(config.getWtfHandler());
        assertTrue(unprocessed.isEmpty());
    }

    /**
     * Test calling {@link ConfigurationFactory#createConfigurationFromArgs(String[])} with a name
     * that does not exist.
     */
    @Test
    public void testCreateConfigurationFromArgs_missing() {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"non existent"});
            fail("did not throw ConfigurationException");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /**
     * Test calling {@link ConfigurationFactory#createConfigurationFromArgs(String[])} with config
     * that has unset mandatory options.
     *
     * <p>Expect this to succeed, since mandatory option validation no longer happens at
     * configuration instantiation time.
     */
    @Test
    public void testCreateConfigurationFromArgs_mandatory() throws ConfigurationException {
        assertNotNull(mFactory.createConfigurationFromArgs(new String[] {"mandatory-config"}));
    }

    /**
     * Test passing empty arg list to {@link
     * ConfigurationFactory#createConfigurationFromArgs(String[])}.
     */
    @Test
    public void testCreateConfigurationFromArgs_empty() {
        try {
            mFactory.createConfigurationFromArgs(new String[] {});
            fail("did not throw ConfigurationException");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /** Test {@link ConfigurationFactory#createConfigurationFromArgs(String[])} using TEST_CONFIG */
    @Test
    public void testCreateConfigurationFromArgs() throws ConfigurationException {
        // pick an arbitrary option to test to ensure it gets populated
        IConfiguration config = mFactory.createConfigurationFromArgs(new String[] {TEST_CONFIG,
                "--log-level", LogLevel.VERBOSE.getStringValue()});
        ILeveledLogOutput logger = config.getLogOutput();
        assertEquals(LogLevel.VERBOSE, logger.getLogLevel());
    }

    /**
     * Test {@link ConfigurationFactory#createConfigurationFromArgs(String[])} when extra positional
     * arguments are supplied
     */
    @Test
    public void testCreateConfigurationFromArgs_unprocessedArgs() {
        try {
            mFactory.createConfigurationFromArgs(new String[] {TEST_CONFIG, "--log-level",
                    LogLevel.VERBOSE.getStringValue(), "blah"});
            fail("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /** Test {@link ConfigurationFactory#printHelp(PrintStream)} */
    @Test
    public void testPrintHelp() {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PrintStream mockPrintStream = new PrintStream(outputStream);
        mRealFactory.printHelp(mockPrintStream);
        // verify all the instrument config names are present
        final String usageString = outputStream.toString();
        assertTrue(usageString.contains("instrument"));
    }

    /**
     * Test {@link ConfigurationFactory#printHelpForConfig(String[], boolean, PrintStream)} when
     * config referenced by args exists
     */
    @Test
    public void testPrintHelpForConfig_configExists() {
        String[] args = new String[] {TEST_CONFIG};
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PrintStream mockPrintStream = new PrintStream(outputStream);
        mFactory.printHelpForConfig(args, true, mockPrintStream);

        // verify the default configs name used is present
        final String usageString = outputStream.toString();
        assertTrue(usageString.contains(TEST_CONFIG));
        // TODO: add stricter verification
    }

    /** Test {@link ConfigurationFactory#getConfigList()} */
    @Test
    public void testListAllConfigs() {
        List<String> listConfigs = mRealFactory.getConfigList();
        assertTrue(listConfigs.size() != 0);
        // Check that our basic configs are always here
        assertTrue(listConfigs.contains("empty"));
        assertTrue(listConfigs.contains("host"));
        assertTrue(listConfigs.contains("instrument"));
    }

    /**
     * Test {@link ConfigurationFactory#getConfigList(String, boolean)} where we list the config in
     * a sub path only
     */
    @Test
    public void testListSubConfig() {
        final String subDir = "suite/";
        List<String> listConfigs = mRealFactory.getConfigList(subDir, false);
        assertTrue(listConfigs.size() != 0);
        // Check that our basic configs are always here
        assertTrue(listConfigs.contains("suite/stub1"));
        assertTrue(listConfigs.contains("suite/stub2"));
        // Validate that all listed config are indeed from the subdir.
        for (String config : listConfigs) {
            assertTrue(config.startsWith(subDir));
        }
    }

    /** Test loading a config that includes another config. */
    @Test
    public void testCreateConfigurationFromArgs_includeConfig() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{INCLUDE_CONFIG});
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        StubOptionTest fromTestConfig = (StubOptionTest) config.getTests().get(0);
        StubOptionTest fromIncludeConfig = (StubOptionTest) config.getTests().get(1);
        assertEquals("valueFromTestConfig", fromTestConfig.mOption);
        assertEquals("valueFromIncludeConfig", fromIncludeConfig.mOption);
    }

    /**
     * Test loading a config that uses the "default" attribute of a template-include tag to include
     * another config.
     */
    @Test
    public void testCreateConfigurationFromArgs_defaultTemplateInclude_default() throws Exception {
        // The default behavior is to include test-config directly.  Nesting is such that innermost
        // elements come first.
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"template-include-config-with-default"});
        assertEquals(2, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(1);
        assertEquals("valueFromTestConfig", innerConfig.mOption);
        assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
    }

    /**
     * Test using {@code <include>} to load a config that uses the "default" attribute of a
     * template-include tag to include a third config.
     */
    @Test
    public void testCreateConfigurationFromArgs_includeTemplateIncludeWithDefault()
            throws Exception {
        // The default behavior is to include test-config directly.  Nesting is such that innermost
        // elements come first.
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"include-template-config-with-default"});
        assertEquals(3, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        assertTrue(config.getTests().get(2) instanceof StubOptionTest);
        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
        StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);
        assertEquals("valueFromTestConfig", innerConfig.mOption);
        assertEquals("valueFromTemplateIncludeWithDefaultConfig", middleConfig.mOption);
        assertEquals("valueFromIncludeTemplateConfigWithDefault", outerConfig.mOption);
    }

    /**
     * Test loading a config that uses the "default" attribute of a template-include tag to include
     * another config. In this case, we override the default attribute on the commandline.
     */
    @Test
    public void testCreateConfigurationFromArgs_defaultTemplateInclude_alternate()
            throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"template-include-config-with-default", "--template:map", "target",
                INCLUDE_CONFIG});
        assertEquals(3, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        assertTrue(config.getTests().get(2) instanceof StubOptionTest);

        StubOptionTest innerConfig = (StubOptionTest) config.getTests().get(0);
        StubOptionTest middleConfig = (StubOptionTest) config.getTests().get(1);
        StubOptionTest outerConfig = (StubOptionTest) config.getTests().get(2);

        assertEquals("valueFromTestConfig", innerConfig.mOption);
        assertEquals("valueFromIncludeConfig", middleConfig.mOption);
        assertEquals("valueFromTemplateIncludeWithDefaultConfig", outerConfig.mOption);
    }

    /** Test loading a config that uses template-include to include another config. */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"template-include-config", "--template:map", "target",
                "test-config"});
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        StubOptionTest fromTestConfig = (StubOptionTest) config.getTests().get(0);
        StubOptionTest fromTemplateIncludeConfig = (StubOptionTest) config.getTests().get(1);
        assertEquals("valueFromTestConfig", fromTestConfig.mOption);
        assertEquals("valueFromTemplateIncludeConfig", fromTemplateIncludeConfig.mOption);
    }

    @Test
    public void testCreateConfigurationFromArgs_repeatedTemplate() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {"repeated-template", "--template:map", "target", "empty"});
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "Failed to parse config xml 'repeated-template'. Reason: Template named "
                            + "'target' appeared more than once.",
                    expected.getMessage());
        }
    }

    /** Test loading a config that uses template-include to include another config. */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_multiKey() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {
                        "template-include-config",
                        "--template:map",
                        "target",
                        "test-config",
                        "--template:map",
                        "target",
                        "test-config"
                    });
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            // Expected
            assertEquals(
                    "More than one template specified for key 'target'", expected.getMessage());
        }
    }

    /** Make sure that we throw a useful error when template-include usage is underspecified. */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_unspecified() throws Exception {
        final String configName = "template-include-config";
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // Make sure that we get the expected error message
            final String msg = e.getMessage();
            assertNotNull(msg);

            assertTrue(String.format("Error message does not mention the name of the broken " +
                    "config.  msg was: %s", msg), msg.contains(configName));

            // Error message should help people to resolve the problem
            assertTrue(String.format("Error message should help user to resolve the " +
                    "template-include.  msg was: %s", msg),
                    msg.contains(String.format("--template:map %s", "target")));
            CLog.e(msg);
            assertTrue(
                    String.format(
                            "Error message should mention the ability to specify a "
                                    + "default resolution.  msg was: %s",
                            msg),
                    msg.contains("'default'"));
        }
    }

    /**
     * Make sure that we throw a useful error when template-include mentions a target configuration
     * that doesn't exist.
     */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_missing() throws Exception {
        final String configName = "template-include-config";
        final String includeName = "no-exist";

        try {
            mFactory.createConfigurationFromArgs(
                    new String[]{configName, "--template:map", "target", includeName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // Make sure that we get the expected error message
            final String msg = e.getMessage();
            assertNotNull(msg);

            assertTrue(String.format("Error message does not mention the name of the broken " +
                    "config.  msg was: %s", msg), msg.contains(configName));
            assertTrue(String.format("Error message does not mention the name of the missing " +
                    "include target.  msg was: %s", msg), msg.contains(includeName));
        }
    }

    /** Test loading a config that includes a local config. */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_local() throws Exception {
        final String configName = "template-include-config";
        InputStream configStream = getClass().getResourceAsStream(
                String.format("/testconfigs/%s.xml", INCLUDE_CONFIG));
        File tmpConfig = FileUtil.createTempFile(INCLUDE_CONFIG, ".xml");
        try {
            FileUtil.writeToFile(configStream, tmpConfig);
            final String includeName = tmpConfig.getAbsolutePath();
            IConfiguration config = mFactory.createConfigurationFromArgs(
                    new String[]{configName, "--template:map", "target", includeName});
            assertTrue(config.getTests().get(0) instanceof StubOptionTest);
            assertTrue(config.getTests().get(1) instanceof StubOptionTest);
            StubOptionTest fromTestConfig = (StubOptionTest) config.getTests().get(0);
            StubOptionTest fromIncludeConfig = (StubOptionTest) config.getTests().get(1);
            assertEquals("valueFromTestConfig", fromTestConfig.mOption);
            assertEquals("valueFromIncludeConfig", fromIncludeConfig.mOption);
        } finally {
            FileUtil.deleteFile(tmpConfig);
        }
    }

    /**
     * This unit test codifies the expectation that an inner {@code <template-include>} tag is
     * properly found and replaced by a config containing another template that is resolved. MAIN
     * CONFIG -> Template 1 -> Template 2 = Works!
     */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_dependent() throws Exception {
        final String configName = "depend-template-include-config";
        final String depTargetName = "template-include-config";
        final String targetName = "test-config";

        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            configName,
                            "--template:map",
                            "dep-target",
                            depTargetName,
                            "--template:map",
                            "target",
                            targetName
                        });
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        assertTrue(config.getTests().get(1) instanceof StubOptionTest);
    }

    /**
     * This unit test codifies the expectation that an inner {@code <template-include>} tag replaced
     * by a missing config raise a failure. MAIN CONFIG -> Template 1 -> Template 2(missing) = error
     */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_dependent_missing()
            throws Exception {
        final String configName = "depend-template-include-config";
        final String depTargetName = "template-include-config";
        final String targetName = "test-config-missing";
        final String expError = String.format(
                "Bundled config '%s' is including a config '%s' that's neither local nor bundled.",
                depTargetName, targetName);

        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "dep-target", depTargetName,
                    "--template:map", "target", targetName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // Make sure that we get the expected error message
            assertEquals(expError, e.getMessage());
        }
    }

    /**
     * This unit test codifies the expectation that an inner {@code <template-include>} tag that
     * doesn't have a default attribute will fail because cannot be resolved. MAIN CONFIG ->
     * Template 1 -> Template 2(no Default, no args override) = error
     */
    @Test
    public void testCreateConfigurationFromArgs_templateInclude_dependent_nodefault()
            throws Exception {
        final String configName = "depend-template-include-config";
        final String depTargetName = "template-include-config";
        final String expError = String.format(
                "Failed to parse config xml '%s'. Reason: " +
                ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
                configName, configName, depTargetName);
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "dep-target", depTargetName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            assertEquals(expError, e.getMessage());
        }
    }

    /**
     * This unit test codifies the expectation that an inner {@code <template-include>} tag that is
     * inside an include tag will be correctly replaced by arguments. Main Config -> Include 1 ->
     * Template 1 -> test-config.xml
     */
    @Test
    public void testCreateConfigurationFromArgs_include_dependent() throws Exception {
        final String configName = "include-template-config";
        final String targetName = "test-config";
        try {
            IConfiguration config = mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "target", targetName});
            assertTrue(config.getTests().get(0) instanceof StubOptionTest);
            assertTrue(config.getTests().get(1) instanceof StubOptionTest);
        } catch (ConfigurationException e) {
            fail(e.getMessage());
        }
    }

    /**
     * This unit test ensure that when a configuration use included configuration the top
     * configuration description is kept.
     */
    @Test
    public void testTopDescriptionIsPreserved() throws ConfigurationException {
        final String configName = "top-config";
        Map<String, String> fakeTemplate = new HashMap<>();
        fakeTemplate.put("target", "included-config");
        ConfigurationDef test = mFactory.new ConfigLoader(false)
                .getConfigurationDef(configName, fakeTemplate);
        assertEquals("top config description", test.getDescription());
    }

    /**
     * This unit test codifies the expectation that an inner {@code <template-include>} tag that is
     * inside an include tag will be correctly rejected if no arguments can match it and no default
     * value is present. Main Config -> Include 1 -> Template 1 (No default, no args override) =
     * error
     */
    @Test
    public void testCreateConfigurationFromArgs_include_dependent_nodefault() throws Exception {
        final String configName = "include-template-config";
        final String includeTargetName = "template-include-config";
        final String expError = String.format(
                "Failed to parse config xml '%s'. Reason: " +
                ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
                configName, configName, includeTargetName);
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            assertEquals(expError, e.getMessage());
        }
    }

    /** Test loading a config that tries to include itself */
    @Test
    public void testCreateConfigurationFromArgs_recursiveInclude() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"recursive-config"});
            fail("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /**
     * Test loading a config that tries to replace a template with itself will fail because it
     * creates a cycle of configuration.
     */
    @Test
    public void testCreateConfigurationFromArgs_recursiveTemplate() throws Exception {
        final String configName = "depend-template-include-config";
        final String depTargetName = "depend-template-include-config";
        final String expError = String.format(
                "Circular configuration include: config '%s' is already included", depTargetName);
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "dep-target", depTargetName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            assertEquals(expError, e.getMessage());
        }
    }

    /**
     * Re-apply a template on a lower config level. Should result in a fail to parse because each
     * template:map can only be applied once. So missing the default will throw exception.
     */
    @Test
    public void testCreateConfigurationFromArgs_template_multilevel() throws Exception {
        final String configName = "depend-template-include-config";
        final String depTargetName = "template-include-config";
        final String depTargetName2 = "template-collision-include-config";
        final String expError = String.format(
                "Failed to parse config xml '%s'. Reason: " +
                ConfigurationXmlParser.ConfigHandler.INNER_TEMPLATE_INCLUDE_ERROR,
                configName, configName, depTargetName2);
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "dep-target", depTargetName,
                    "--template:map", "target", depTargetName2});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            assertEquals(expError, e.getMessage());
        }
    }

    /**
     * Re-apply a template twice. Should result in an error only because the configs included have
     * several build_provider
     */
    @Test
    public void testCreateConfigurationFromArgs_templateCollision() throws Exception {
        final String configName = "template-collision-include-config";
        final String depTargetName = "template-include-config-with-default";
        final String expError =
                "Failed to parse config xml 'template-collision-include-config'. Reason: "
                        + "Template named 'target' appeared more than once.";
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", "target-col", depTargetName,
                    "--template:map", "target-col2", depTargetName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            assertEquals(expError, e.getMessage());
        }
    }

    /** Test loading a config that tries to include a non-bundled config. */
    @Test
    public void testCreateConfigurationFromArgs_nonBundledInclude() throws Exception {
       try {
           mFactory.createConfigurationFromArgs(new String[] {"non-bundled-config"});
           fail("ConfigurationException not thrown");
       } catch (ConfigurationException e) {
           // expected
       }
    }

    /** Test reloading a config after it has been updated. */
    @Test
    public void testCreateConfigurationFromArgs_localConfigReload()
            throws ConfigurationException, IOException {
        File localConfigFile = FileUtil.createTempFile("local-config", ".xml");
        try {
            // Copy the config to the local filesystem
            InputStream source = getClass().getResourceAsStream("/testconfigs/local-config.xml");
            FileUtil.writeToFile(source, localConfigFile);

            // Depending on the system, file modification times might not have greater than 1 second
            // resolution. Backdate the original contents so that when we write to the file later,
            // it shows up as a new change.
            localConfigFile.setLastModified(System.currentTimeMillis() - 5000);

            // Load the configuration from the local file
            IConfiguration config = mFactory.createConfigurationFromArgs(
                    new String[] { localConfigFile.getAbsolutePath() });
            if (!(config.getTests().get(0) instanceof StubOptionTest)) {
                fail(String.format("Expected a StubOptionTest, but got %s",
                        config.getTests().get(0).getClass().getName()));
                return;
            }
            StubOptionTest test = (StubOptionTest)config.getTests().get(0);
            assertEquals("valueFromOriginalConfig", test.mOption);

            // Change the contents of the local file
            source = getClass().getResourceAsStream("/testconfigs/local-config-update.xml");
            FileUtil.writeToFile(source, localConfigFile);

            // Get the configuration again and verify that it picked up the update
            config = mFactory.createConfigurationFromArgs(
                    new String[] { localConfigFile.getAbsolutePath() });
            if (!(config.getTests().get(0) instanceof StubOptionTest)) {
                fail(String.format("Expected a StubOptionTest, but got %s",
                        config.getTests().get(0).getClass().getName()));
                return;
            }
            test = (StubOptionTest)config.getTests().get(0);
            assertEquals("valueFromUpdatedConfig", test.mOption);
        } finally {
            FileUtil.deleteFile(localConfigFile);
        }
    }

    /** Test loading a config that has a circular include */
    @Test
    public void testCreateConfigurationFromArgs_circularInclude() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"circular-config"});
            fail("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /**
     * If a template:map argument is passed but doesn't match any {@code <template-include>} tag a
     * configuration exception will be thrown for unmatched arguments.
     */
    @Test
    public void testCreateConfigurationFromArgs_templateName_notExist() throws Exception {
        final String configName = "include-template-config-with-default";
        final String targetName = "test-config";
        final String missingNameTemplate = "NOTEXISTINGNAME";
        Map<String, String> expected = new HashMap<String,String>();
        expected.put(missingNameTemplate, targetName);
        final String expError = String.format(
                "Unused template:map parameters: %s", expected);

        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", missingNameTemplate, targetName});
            fail ("ConfigurationException not thrown");
        } catch (ConfigurationException e) {
            // Make sure that we get the expected error message
            assertEquals(expError, e.getMessage());
        }
    }

    /**
     * If a configuration is called a second time, ensure that the cached config is also properly
     * returned, and that template:map did not cause issues.
     */
    @Test
    public void testCreateConfigurationFromArgs_templateName_notExistTest() throws Exception {
        final String configName = "template-include-config-with-default";
        final String targetName = "local-config";
        final String nameTemplate = "target";
        IConfiguration tmp = null;
        try {
            tmp = mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", nameTemplate, targetName});
        } catch (ConfigurationException e) {
            fail("ConfigurationException thrown: " + e.getMessage());
        }
        assertTrue(tmp.getTests().size() == 2);

        // Call the same config a second time to make sure the cached version works.
        try {
            tmp = mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", nameTemplate, targetName});
        } catch (ConfigurationException e) {
            fail("ConfigurationException thrown: " + e.getMessage());
        }
        assertTrue(tmp.getTests().size() == 2);
    }

    /**
     * If a configuration is called a second time with bad template name, it should still throw the
     * unused config template:map
     */
    @Test
    public void testCreateConfigurationFromArgs_templateName_stillThrow() throws Exception {
        final String configName = "template-include-config-with-default";
        final String targetName = "local-config";
        final String nameTemplate = "target_not_exist";
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", nameTemplate, targetName});
            fail("ConfigurationException should have been thrown");
        } catch (ConfigurationException e) {
            // expected
        }

        // Call the same config a second time to make sure it is also rejected.
        try {
            mFactory.createConfigurationFromArgs(new String[]{configName,
                    "--template:map", nameTemplate, targetName});
            fail("ConfigurationException should have been thrown");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /** Parse a config with 3 different device configuration specified. */
    @Test
    public void testCreateConfigurationFromArgs_multidevice() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"multi-device"});
        assertEquals(1, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        // Verify that all attributes are in the right place:
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals("10", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(0, config.getDeviceConfigByName("device1")
                .getTargetPreparers().size());

        assertNotNull(config.getDeviceConfigByName("device2"));
        assertEquals("0", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(1, config.getDeviceConfigByName("device2")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device2")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);

        assertNotNull(config.getDeviceConfigByName("device3"));
        assertEquals("0", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("build-flavor3", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildFlavor());
        assertEquals(2, config.getDeviceConfigByName("device3")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device3")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);
    }

    /**
     * Test that if an object inside a <device> tag is implementing {@link IConfigurationReceiver}
     * it will receives the config properly like non-device object.
     */
    @Test
    public void testCreateConfigurationFromArgs_injectConfiguration() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(new String[] {"multi-device"});
        assertEquals(1, config.getTests().size());

        assertNotNull(config.getDeviceConfigByName("device2"));
        assertEquals(1, config.getDeviceConfigByName("device2").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device2").getTargetPreparers().get(0)
                        instanceof StubTargetPreparer);
        StubTargetPreparer stubDevice2 =
                (StubTargetPreparer)
                        config.getDeviceConfigByName("device2").getTargetPreparers().get(0);
        assertEquals(config, stubDevice2.getConfiguration());

        assertNotNull(config.getDeviceConfigByName("device3"));
        assertEquals(2, config.getDeviceConfigByName("device3").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device3").getTargetPreparers().get(0)
                        instanceof StubTargetPreparer);
        StubTargetPreparer stubDevice3 =
                (StubTargetPreparer)
                        config.getDeviceConfigByName("device3").getTargetPreparers().get(0);
        assertEquals(config, stubDevice3.getConfiguration());
    }

    /**
     * Parse a config with 3 different device configuration specified. And apply a command line to
     * override some attributes.
     */
    @Test
    public void testCreateConfigurationFromArgs_multidevice_applyCommandLine() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"multi-device", "--{device2}build-id","20", "--{device1}null-device",
                        "--{device3}com.android.tradefed.build.StubBuildProvider:build-id","30"});
        assertEquals(1, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        // Verify that all attributes are in the right place:
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals("10", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(0, config.getDeviceConfigByName("device1")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device1")
                .getDeviceRequirements().nullDeviceRequested());

        assertNotNull(config.getDeviceConfigByName("device2"));
        // Device2 build provider is modified independently
        assertEquals("20", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(1, config.getDeviceConfigByName("device2")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device2")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);
        assertFalse(config.getDeviceConfigByName("device2")
                .getDeviceRequirements().nullDeviceRequested());

        // Device3 build provider is modified independently
        assertNotNull(config.getDeviceConfigByName("device3"));
        assertEquals("30", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("build-flavor3", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildFlavor());
        assertEquals(2, config.getDeviceConfigByName("device3")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device3")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);
        assertFalse(config.getDeviceConfigByName("device3")
                .getDeviceRequirements().nullDeviceRequested());
    }

    /**
     * Parse a config with 3 different device configuration specified. And apply a command line to
     * override some attributes.
     */
    @Test
    public void testCreateConfigurationFromArgs_multidevice_singletag() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "multi-device-empty",
                            "--{device2}build-id",
                            "20",
                            "--{device1}null-device",
                            "--{device3}com.android.tradefed.build.StubBuildProvider:build-id",
                            "30"
                        });
        assertEquals(1, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        // Verify that all attributes are in the right place:
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals(
                "0",
                config.getDeviceConfigByName("device1").getBuildProvider().getBuild().getBuildId());
        assertEquals(
                "stub",
                config.getDeviceConfigByName("device1").getBuildProvider().getBuild().getTestTag());
        assertEquals(0, config.getDeviceConfigByName("device1").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device1")
                        .getDeviceRequirements()
                        .nullDeviceRequested());

        assertNotNull(config.getDeviceConfigByName("device2"));
        // Device2 build provider is modified independently
        assertEquals(
                "20",
                config.getDeviceConfigByName("device2").getBuildProvider().getBuild().getBuildId());
        assertEquals(
                "stub",
                config.getDeviceConfigByName("device2").getBuildProvider().getBuild().getTestTag());
        assertEquals(0, config.getDeviceConfigByName("device2").getTargetPreparers().size());
        assertFalse(
                config.getDeviceConfigByName("device2")
                        .getDeviceRequirements()
                        .nullDeviceRequested());

        // Device3 build provider is modified independently
        assertNotNull(config.getDeviceConfigByName("device3"));
        assertEquals(
                "30",
                config.getDeviceConfigByName("device3").getBuildProvider().getBuild().getBuildId());
        assertEquals(0, config.getDeviceConfigByName("device3").getTargetPreparers().size());
        assertFalse(
                config.getDeviceConfigByName("device3")
                        .getDeviceRequirements()
                        .nullDeviceRequested());
    }

    /**
     * Parse a config with 3 different device configuration specified. And apply a command line to
     * override all attributes.
     */
    @Test
    public void testCreateConfigurationFromArgs_multidevice_applyCommandLineGlobal()
            throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"multi-device", "--build-id","20"});
        assertEquals(1, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        // Verify that all attributes are in the right place:
        // All build id are now modified since option had a global scope
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals("20", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device1")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(0, config.getDeviceConfigByName("device1")
                .getTargetPreparers().size());

        assertNotNull(config.getDeviceConfigByName("device2"));
        assertEquals("20", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName("device2")
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(1, config.getDeviceConfigByName("device2")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device2")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);

        assertNotNull(config.getDeviceConfigByName("device3"));
        assertEquals("20", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("build-flavor3", config.getDeviceConfigByName("device3")
                .getBuildProvider().getBuild().getBuildFlavor());
        assertEquals(2, config.getDeviceConfigByName("device3")
                .getTargetPreparers().size());
        assertTrue(config.getDeviceConfigByName("device3")
                .getTargetPreparers().get(0) instanceof StubTargetPreparer);
    }

    /**
     * Test that when <device> tags are out of order (device 1 - device 2 - device 1) and an option
     * is specified in the last device 1 with an increased frequency (a same class object from the
     * first device 1 or 2), the option is properly found and assigned.
     */
    @Test
    public void testCreateConfigurationFromArgs_frequency() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(new String[] {"multi-device-mix"});
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals(3, config.getDeviceConfigByName("device1").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device1").getTargetPreparers().get(0)
                        instanceof DeviceWiper);
        DeviceWiper prep1 =
                (DeviceWiper) config.getDeviceConfigByName("device1").getTargetPreparers().get(0);
        assertTrue(prep1.isDisabled());
        assertTrue(
                config.getDeviceConfigByName("device1").getTargetPreparers().get(2)
                        instanceof DeviceWiper);
        DeviceWiper prep3 =
                (DeviceWiper) config.getDeviceConfigByName("device1").getTargetPreparers().get(2);
        assertFalse(prep3.isDisabled());

        assertNotNull(config.getDeviceConfigByName("device2"));
        assertEquals(1, config.getDeviceConfigByName("device2").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device2").getTargetPreparers().get(0)
                        instanceof DeviceWiper);
        DeviceWiper prep2 =
                (DeviceWiper) config.getDeviceConfigByName("device2").getTargetPreparers().get(0);
        // Only device 1 preparer has been targeted.
        assertTrue(prep2.isDisabled());
    }

    /**
     * Tests a different usage of options for a multi device interleaved config where options are
     * specified.
     */
    @Test
    public void testCreateConfigurationFromArgs_frequency_withOptionOpen() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(new String[] {"multi-device-mix-options"});
        assertNotNull(config.getDeviceConfigByName("device1"));
        assertEquals(3, config.getDeviceConfigByName("device1").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device1").getTargetPreparers().get(0)
                        instanceof DeviceWiper);
        DeviceWiper prep1 =
                (DeviceWiper) config.getDeviceConfigByName("device1").getTargetPreparers().get(0);
        assertTrue(prep1.isDisabled());
        assertTrue(
                config.getDeviceConfigByName("device1").getTargetPreparers().get(2)
                        instanceof DeviceWiper);
        DeviceWiper prep3 =
                (DeviceWiper) config.getDeviceConfigByName("device1").getTargetPreparers().get(2);
        assertFalse(prep3.isDisabled());

        assertNotNull(config.getDeviceConfigByName("device2"));
        assertEquals(1, config.getDeviceConfigByName("device2").getTargetPreparers().size());
        assertTrue(
                config.getDeviceConfigByName("device2").getTargetPreparers().get(0)
                        instanceof DeviceWiper);
        DeviceWiper prep2 =
                (DeviceWiper) config.getDeviceConfigByName("device2").getTargetPreparers().get(0);
        // Only device 1 preparer has been targeted.
        assertTrue(prep2.isDisabled());
    }

    /**
     * Configuration for multi device is wrong since it contains a build_provider tag outside the
     * devices tags.
     */
    @Test
    public void testCreateConfigurationFromArgs_multidevice_exception() throws Exception {
        String expectedException =
                "You seem to want a multi-devices configuration but you have "
                        + "[build_provider] tags outside the <device> tags";
        try {
            mFactory.createConfigurationFromArgs(new String[]{"multi-device-outside-tag"});
            fail("Should have thrown a Configuration Exception");
        } catch(ConfigurationException e) {
            assertEquals(expectedException, e.getMessage());
        }
    }

    /**
     * Parse a config with no multi device config, and expect the new device holder to still be
     * there and adding a default device.
     */
    @Test
    public void testCreateConfigurationFromArgs_old_config_with_deviceHolder() throws Exception {
        IConfiguration config = mFactory.createConfigurationFromArgs(
                new String[]{"test-config", "--build-id","20", "--serial", "test"});
        assertEquals(1, config.getTests().size());
        assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        // Verify that all attributes are in the right place:
        // All build id are now modified since option had a global scope
        assertNotNull(config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME));
        assertEquals("20", config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME)
                .getBuildProvider().getBuild().getBuildId());
        assertEquals("stub", config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME)
                .getBuildProvider().getBuild().getTestTag());
        assertEquals(1, config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME)
                .getTargetPreparers().size());
        List<String> serials = new ArrayList<String>();
        serials.add("test");
        assertEquals(serials, config.getDeviceRequirements().getSerials(null));
        assertEquals(
                serials,
                config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME)
                        .getDeviceRequirements()
                        .getSerials(null));
    }

    /**
     * Test that when parsing command line options, boolean options with Device tag and namespace
     * are correctly assigned.
     */
    @Test
    public void testCreateConfiguration_injectDeviceBooleanOption() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "test-config-multi",
                            "--{device1}no-test-boolean-option",
                            "--{device1}test-boolean-option-false",
                            // testing with namespace too
                            "--{device2}stub-preparer:no-test-boolean-option",
                            "--{device2}stub-preparer:test-boolean-option-false"
                        });
        assertEquals(2, config.getDeviceConfig().size());
        IDeviceConfiguration device1 = config.getDeviceConfigByName("device1");
        StubTargetPreparer deviceSetup1 = (StubTargetPreparer) device1.getTargetPreparers().get(0);
        // default value of test-boolean-option is true, we set it to false
        assertFalse(deviceSetup1.getTestBooleanOption());
        // default value of test-boolean-option-false is false, we set it to true.
        assertTrue(deviceSetup1.getTestBooleanOptionFalse());

        IDeviceConfiguration device2 = config.getDeviceConfigByName("device2");
        StubTargetPreparer deviceSetup2 = (StubTargetPreparer) device2.getTargetPreparers().get(0);
        assertFalse(deviceSetup2.getTestBooleanOption());
        assertTrue(deviceSetup2.getTestBooleanOptionFalse());
    }

    /** Test that when an <include> tag is used inside a <device> tag we correctly resolve it. */
    @Test
    public void testCreateConfiguration_includeInDevice() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {"test-config-multi-include", "--test-dir", "faketestdir"});
        assertEquals(2, config.getDeviceConfig().size());
        IDeviceConfiguration device1 = config.getDeviceConfigByName("device1");
        assertTrue(device1.getTargetPreparers().get(0) instanceof StubTargetPreparer);
        // The included config in device2 loads a different build_provider
        IDeviceConfiguration device2 = config.getDeviceConfigByName("device2");
        assertTrue(device2.getBuildProvider() instanceof LocalDeviceBuildProvider);
        LocalDeviceBuildProvider provider = (LocalDeviceBuildProvider) device2.getBuildProvider();
        // command line options are properly propagated to the included object in device tag.
        assertEquals("faketestdir", provider.getTestDir().getName());
    }

    /**
     * Test when an <include> tag tries to load a <device> tag inside another <device> tag. This
     * should throw an exception.
     */
    @Test
    public void testCreateConfiguration_includeInDevice_inDevice() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {
                        "multi-device-incorrect-include",
                    });
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "<device> tag cannot be included inside another device", expected.getMessage());
        }
    }

    /** Test that {@link ConfigurationFactory#reorderArgs(String[])} is properly reordering args. */
    @Test
    public void testReorderArgs_check_ordering() throws Throwable {
        String[] args =
                new String[] {
                    "config",
                    "--option1",
                    "o1",
                    "--template:map",
                    "tm=tm1",
                    "--option2",
                    "--option3",
                    "o3",
                    "--template:map",
                    "tm",
                    "tm2"
                };
        String[] wantArgs =
                new String[] {
                    "config",
                    "--template:map",
                    "tm=tm1",
                    "--template:map",
                    "tm",
                    "tm2",
                    "--option1",
                    "o1",
                    "--option2",
                    "--option3",
                    "o3"
                };

        assertEquals(Arrays.toString(wantArgs), Arrays.toString(mFactory.reorderArgs(args)));
    }

    /**
     * Test that {@link ConfigurationFactory#reorderArgs(String[])} properly handles a short arg
     * after a template arg.
     */
    @Test
    public void testReorderArgs_template_with_short_arg() throws Throwable {
        String[] args =
                new String[] {
                    "config",
                    "--option1",
                    "o1",
                    "--template:map",
                    "tm=tm1",
                    "-option2",
                    "--option3",
                    "o3",
                    "--template:map",
                    "tm",
                    "tm2"
                };
        String[] wantArgs =
                new String[] {
                    "config",
                    "--template:map",
                    "tm=tm1",
                    "--template:map",
                    "tm",
                    "tm2",
                    "--option1",
                    "o1",
                    "-option2",
                    "--option3",
                    "o3"
                };

        assertEquals(Arrays.toString(wantArgs), Arrays.toString(mFactory.reorderArgs(args)));
    }

    /**
     * Test that {@link ConfigurationFactory#reorderArgs(String[])} properly handles a incomplete
     * template arg.
     */
    @Test
    public void testReorderArgs_incomplete_template_arg() throws Throwable {
        String[] args =
                new String[] {
                    "config",
                    "--option1",
                    "o1",
                    "--template:map",
                    "tm=tm1",
                    "-option2",
                    "--option3",
                    "o3",
                    "--template:map",
                };
        String[] wantArgs =
                new String[] {
                    "config",
                    "--template:map",
                    "tm=tm1",
                    "--template:map",
                    "--option1",
                    "o1",
                    "-option2",
                    "--option3",
                    "o3"
                };

        assertEquals(Arrays.toString(wantArgs), Arrays.toString(mFactory.reorderArgs(args)));
    }

    /**
     * Test that when doing a dry-run with keystore arguments, we skip the keystore validation. We
     * accept the argument, as long as the key exists.
     */
    @Test
    public void testCreateConfigurationFromArgs_dryRun_keystore() throws Exception {
        IConfiguration res =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "test-config",
                            "--build-id",
                            "USE_KEYSTORE@test_string",
                            "--dry-run",
                            "--online-wait-time=USE_KEYSTORE@test_long",
                            "--min-battery-after-recovery",
                            "USE_KEYSTORE@test_int",
                            "--disable-unresponsive-reboot=USE_KEYSTORE@test_boolean",
                        });
        res.validateOptions();
        // we still throw exception if the option itself doesn't exists.
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {
                        "test-config", "--does-not-exists", "USE_KEYSTORE@test_string", "--dry-run"
                    });
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            // expected
        }
    }

    /**
     * Test that when mandatory option are set with a keystore during a dry-run, they can still be
     * validated.
     */
    @Test
    public void testCreateConfigurationFromArgs_dryRun_keystore_required_arg() throws Exception {
        IConfiguration res =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "mandatory-config",
                            "--build-dir",
                            "USE_KEYSTORE@test_string",
                            "--dry-run",
                        });
        // Check that mandatory option was properly set, otherwise it will throw.
        res.validateOptions();
    }

    /**
     * This unit test ensures that the code will search for missing test configs in directories
     * specified in certain environment variables.
     */
    @Test
    public void testSearchConfigFromEnvVar() throws IOException {
        File externalConfig = FileUtil.createTempFile("external-config", ".config");
        String configName = FileUtil.getBaseName(externalConfig.getName());
        File tmpDir = externalConfig.getParentFile();

        ConfigurationFactory spyFactory = Mockito.spy(mFactory);
        Mockito.doReturn(Arrays.asList(tmpDir)).when(spyFactory).getExternalTestCasesDirs();

        try {
            File config = spyFactory.getTestCaseConfigPath(configName);
            assertEquals(config.getAbsolutePath(), externalConfig.getAbsolutePath());
        } finally {
            FileUtil.deleteFile(externalConfig);
        }
    }

    /**
     * This unit test ensures that the code will search for missing test configs in directories
     * specified in certain environment variables, and fail as the test config still can't be found.
     */
    @Test
    public void testSearchConfigFromEnvVarFailed() throws Exception {
        File tmpDir = FileUtil.createTempDir("config-check-var");
        try {
            ConfigurationFactory spyFactory = Mockito.spy(mFactory);
            Mockito.doReturn(Arrays.asList(tmpDir)).when(spyFactory).getExternalTestCasesDirs();
            File config = spyFactory.getTestCaseConfigPath("non-exist");
            assertNull(config);
            Mockito.verify(spyFactory, Mockito.times(1)).getExternalTestCasesDirs();
        } finally {
            FileUtil.recursiveDelete(tmpDir);
        }
    }

    /**
     * Tests that {@link ConfigurationFactory#getConfigNamesFromTestCases(String)} returns the
     * proper files of the subpath only.
     */
    @Test
    public void testGetConfigNamesFromTestCases_subpath() throws Exception {
        File tmpDir = FileUtil.createTempDir("test-config-dir");
        try {
            File config1 = FileUtil.createTempFile("testconfig1", ".config", tmpDir);
            FileUtil.writeToFile("<configuration></configuration>", config1);
            File subDir = FileUtil.createTempDir("subdir", tmpDir);
            File config2 = FileUtil.createTempFile("testconfig2", ".xml", subDir);
            FileUtil.writeToFile("<configuration></configuration>", config2);
            ConfigurationFactory spyFactory = Mockito.spy(mFactory);
            Mockito.doReturn(Arrays.asList(tmpDir)).when(spyFactory).getExternalTestCasesDirs();
            // looking at full path we get both configs
            Set<String> res = spyFactory.getConfigNamesFromTestCases(null);
            assertEquals(2, res.size());
            res = spyFactory.getConfigNamesFromTestCases(subDir.getName());
            assertEquals(1, res.size());
            assertTrue(res.iterator().next().contains("testconfig2"));
        } finally {
            FileUtil.recursiveDelete(tmpDir);
        }
    }

    /**
     * Test that if the base configuration is single device and a template attempt to make it
     * multi-devices, it will fail and provide a clear message about which tags are not in the right
     * place.
     */
    @Test
    public void testCreateConfigurationFromArgs_singleDeviceTemplate_includeMulti()
            throws Exception {
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {
                        "local-preparer-base", "--template:map", "config", "test-config-multi"
                    });
            fail("Should have thrown an exception");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "You seem to want a multi-devices configuration but you have "
                            + "[target_preparer] tags outside the <device> tags",
                    expected.getMessage());
        }
    }

    /**
     * Opposite scenario of {@link
     * #testCreateConfigurationFromArgs_singleDeviceTemplate_includeMulti()} where the base template
     * is multi-devices and the template included has a single device tag.
     */
    @Test
    public void testCreateConfigurationFromArgs_multiDeviceTemplate_includeSingle()
            throws Exception {
        try {
            mFactory.createConfigurationFromArgs(
                    new String[] {
                        "multi-device", "--template:map", "preparers", "local-preparer-base"
                    });
            fail("Should have thrown an exception");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "You seem to want a multi-devices configuration but you have "
                            + "[target_preparer] tags outside the <device> tags",
                    expected.getMessage());
        }
    }

    /**
     * Test that if we load a configuration with some unfound class, we throw an exception and
     * report the objects that did not load.
     */
    @Test
    public void testCreateConfigurationFromArgs_failedToLoadClass() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"multi-device-incorrect"});
            fail("Should have thrown an exception");
        } catch (ClassNotFoundConfigurationException expected) {
            assertTrue(
                    expected.getMessage()
                            .contains(
                                    "Failed to load some objects in the configuration "
                                            + "'multi-device-incorrect':"));
            assertEquals(
                    "device1:build_provider",
                    expected.getRejectedObjects().get("com.android.tradefed.build.doesnotexists"));
            assertEquals(
                    "device3:target_preparer",
                    expected.getRejectedObjects()
                            .get("com.android.tradefed.targetprep.doesnotexistseither"));
        }
    }

    /**
     * Test that a configuration with one real device and one fake device (isFake=true) will be
     * loaded like a single device config: The objects outside the <device> tags will be part of the
     * real device.
     */
    @Test
    public void testCreateConfiguration_multiDevice_fake() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "test-config-multi-fake",
                            "--{device1}no-test-boolean-option",
                            "--{device1}test-boolean-option-false",
                            // testing with namespace too
                            "--{device1}stub-preparer:no-test-boolean-option",
                            "--{device1}stub-preparer:test-boolean-option-false"
                        });
        assertEquals(2, config.getDeviceConfig().size());
        IDeviceConfiguration device1 = config.getDeviceConfigByName("device1");
        // One target preparer from inside the device tag, one from outside.
        assertEquals(2, device1.getTargetPreparers().size());
        StubTargetPreparer deviceSetup1 = (StubTargetPreparer) device1.getTargetPreparers().get(0);
        // default value of test-boolean-option is true, we set it to false
        assertFalse(deviceSetup1.getTestBooleanOption());
        // default value of test-boolean-option-false is false, we set it to true.
        assertTrue(deviceSetup1.getTestBooleanOptionFalse());

        // Check that the second preparer, outside device1 can still receive option as {device1}.
        StubTargetPreparer deviceSetup2 = (StubTargetPreparer) device1.getTargetPreparers().get(1);
        // default value of test-boolean-option is true, we set it to false
        assertFalse(deviceSetup2.getTestBooleanOption());
        // default value of test-boolean-option-false is false, we set it to true.
        assertTrue(deviceSetup2.getTestBooleanOptionFalse());

        assertFalse(config.isDeviceConfiguredFake("device1"));
        assertTrue(config.isDeviceConfiguredFake("device2"));
    }

    /**
     * Test when a single device configuration (standard flat) add a fake device. Configuration
     * objects should be added to the default device.
     */
    @Test
    public void testCreateConfiguration_singleDeviceConfig_withFake() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "single-config-and-fake",
                            "--no-test-boolean-option",
                            "--test-boolean-option-false",
                            // testing with namespace too
                            "--stub-preparer:no-test-boolean-option",
                            "--stub-preparer:test-boolean-option-false"
                        });
        assertEquals(2, config.getDeviceConfig().size());
        // Ensure the first device is the default one.
        assertEquals(
                ConfigurationDef.DEFAULT_DEVICE_NAME,
                config.getDeviceConfig().get(0).getDeviceName());

        IDeviceConfiguration device1 =
                config.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME);
        // One target preparer from inside the device tag, one from outside.
        assertEquals(2, device1.getTargetPreparers().size());
        StubTargetPreparer deviceSetup1 = (StubTargetPreparer) device1.getTargetPreparers().get(0);
        // default value of test-boolean-option is true, we set it to false
        assertFalse(deviceSetup1.getTestBooleanOption());
        // default value of test-boolean-option-false is false, we set it to true.
        assertTrue(deviceSetup1.getTestBooleanOptionFalse());
        assertTrue(device1.getDeviceRequirements().gceDeviceRequested());
        assertFalse(device1.getDeviceRequirements().nullDeviceRequested());

        // Check that the second preparer, outside device1 can still receive option as {device1}.
        StubTargetPreparer deviceSetup2 = (StubTargetPreparer) device1.getTargetPreparers().get(1);
        // default value of test-boolean-option is true, we set it to false
        assertFalse(deviceSetup2.getTestBooleanOption());
        // default value of test-boolean-option-false is false, we set it to true.
        assertTrue(deviceSetup2.getTestBooleanOptionFalse());

        assertFalse(config.isDeviceConfiguredFake(ConfigurationDef.DEFAULT_DEVICE_NAME));
        assertTrue(config.isDeviceConfiguredFake("device2"));
        IDeviceConfiguration device2 = config.getDeviceConfigByName("device2");
        assertFalse(device2.getDeviceRequirements().gceDeviceRequested());
        assertTrue(device2.getDeviceRequirements().nullDeviceRequested());
    }

    /** Test that a configuration with all the device marked as isReal=false will be rejected. */
    @Test
    public void testCreateConfiguration_multiDevice_real_notReal() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"test-config-real-not-real"});
            fail("Should have thrown an exception");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "Failed to parse config xml 'test-config-real-not-real'. Reason: Mismatch for "
                            + "device 'device1'. It was defined once as isFake=false, once as "
                            + "isFake=true",
                    expected.getMessage());
        }
    }

    /**
     * Test that a configuration with two real devices and one fake one (isFake=false) will reject
     * object that are at the root: We cannot decide where to put these objects.
     */
    @Test
    public void testCreateConfiguration_multiDevice_twoReal_oneFake() throws Exception {
        try {
            mFactory.createConfigurationFromArgs(new String[] {"test-config-multi-3-fake"});
            fail("Should have thrown an exception");
        } catch (ConfigurationException expected) {
            assertEquals(
                    "You seem to want a multi-devices configuration but you have [target_preparer] "
                            + "tags outside the <device> tags",
                    expected.getMessage());
        }
    }

    /** Class to test out lab preparer parsing */
    public static final class TestLabPreparer extends StubTargetPreparer implements ILabPreparer {}

    @Test
    public void testParse_labPreparer() throws Exception {
        String normalConfig =
                "<configuration description=\"desc\" >\n"
                        + "  <lab_preparer class=\""
                        + TestLabPreparer.class.getName()
                        + "\">\n"
                        + "     <option name=\"test-boolean-option\" value=\"false\"/>"
                        + "  </lab_preparer>\n"
                        + "</configuration>";
        File tmpConfig = FileUtil.createTempFile("tmp-config-tests", ".xml");
        try {
            FileUtil.writeToFile(normalConfig, tmpConfig);
            IConfiguration config =
                    mFactory.createConfigurationFromArgs(
                            new String[] {tmpConfig.getAbsolutePath()});
            assertEquals(1, config.getLabPreparers().size());
            assertFalse(
                    ((StubTargetPreparer) config.getLabPreparers().get(0)).getTestBooleanOption());
        } finally {
            FileUtil.deleteFile(tmpConfig);
        }
    }

    /**
     * Test that even if multi_pre_target_prep and multi_target_prep share the same type, we do not
     * mix the objects internally.
     */
    @Test
    public void testParse_multiTargetPrep() throws Exception {
        String normalConfig =
                "<configuration description=\"desc\" >\n"
                        + "  <multi_pre_target_preparer class=\""
                        + StubMultiTargetPreparer.class.getName()
                        + "\" />\n"
                        + "  <multi_target_preparer class=\""
                        + StubMultiTargetPreparer.class.getName()
                        + "\" />\n"
                        + "</configuration>";
        File tmpConfig = FileUtil.createTempFile("tmp-config-tests", ".xml");
        try {
            FileUtil.writeToFile(normalConfig, tmpConfig);
            IConfiguration config =
                    mFactory.createConfigurationFromArgs(
                            new String[] {tmpConfig.getAbsolutePath()});
            assertEquals(1, config.getMultiPreTargetPreparers().size());
            assertEquals(1, config.getMultiTargetPreparers().size());
            // Different objects have been created for each.
            assertNotSame(
                    config.getMultiPreTargetPreparers().get(0),
                    config.getMultiTargetPreparers().get(0));
        } finally {
            FileUtil.deleteFile(tmpConfig);
        }
    }

    /** Test that an unexpected extension for a config file doesn't parse. */
    @Test
    public void testParseUnexpectedFormat() throws Exception {
        File testConfigFile = FileUtil.createTempFile("test-config-file", ".txt");
        try {
            mFactory.createConfigurationFromArgs(new String[] {testConfigFile.getAbsolutePath()});
            fail("Should have thrown an exception");
        } catch (ConfigurationException expected) {
            assertTrue(expected.getMessage().contains("not supported."));
        } finally {
            FileUtil.deleteFile(testConfigFile);
        }
    }

    /** Test that a YAML config command line parse correctly. */
    @Test
    public void testCreateConfigurationFromArgs_yaml() throws Exception {
        IConfiguration config =
                mFactory.createConfigurationFromArgs(
                        new String[] {
                            "yaml/test-config.tf_yaml",
                            "--build-id",
                            "5",
                            "--build-flavor",
                            "test",
                            "--branch",
                            "main"
                        });
        assertNotNull(config);
        IBuildProvider provider = config.getBuildProvider();
        assertTrue(provider instanceof IDeviceBuildProvider);
        IBuildInfo info = ((IDeviceBuildProvider) provider).getBuild(null);
        try {
            assertEquals("5", info.getBuildId());
            assertEquals("test", info.getBuildFlavor());
            assertEquals("main", info.getBuildBranch());
        } finally {
            info.cleanUp();
        }
    }

    /**
     * Test that the direct config regex is working as expected
     *
     * <p>This test contains examples that should fail the regex.
     */
    @Test
    public void testIsDirectConfigurationFalses() throws Exception {
        Map<String, Boolean> testMatrixMap =
                Map.ofEntries(
                        entry("yaml/test-config.tf_yaml", false),
                        entry("suite/test-mapping", false),
                        entry(
                                "./out/host/linux-x86/testcases/HelloWorldHostTest/HelloWorldHostTest.config",
                                false),
                        entry("gs/proxy-config", false));

        for (Map.Entry<String, Boolean> entry : testMatrixMap.entrySet()) {
            assertEquals(entry.getValue(), mFactory.isDirectConfiguration(entry.getKey()));
        }
    }

    /**
     * Test that the direct config regex is working as expected
     *
     * <p>This test contains examples that should match the regex.
     */
    @Test
    public void testIsDirectConfigurationTrues() throws Exception {
        Map<String, Boolean> testMatrixMap =
                Map.ofEntries(
                        entry("gs://tradefed_test_resources/configs/HelloWorldHostTest.xml", true),
                        entry("gs://other_teams_bucket/SomeOtherTest.xml", true),
                        entry(
                                "https://android-build.googleplex.com/objects/configs/SomeABConfig.xml",
                                true),
                        entry("http://source.android.com/code/AConfig.config", true),
                        entry("file://localhost/home/tradefed/config.xml", true));

        for (Map.Entry<String, Boolean> entry : testMatrixMap.entrySet()) {
            assertEquals(entry.getValue(), mFactory.isDirectConfiguration(entry.getKey()));
        }
    }

    /** Test that the direct configuration method is minimally working */
    @Test
    public void testLoadDirectConfiguration() throws Exception {
        ConfigurationFactory spyFactory = Mockito.spy(mRealFactory);
        // extract the test-config.xml into a tmp file
        InputStream configStream =
                getClass().getResourceAsStream(String.format("/testconfigs/%s.xml", TEST_CONFIG));
        File tmpFile = FileUtil.createTempFile(TEST_CONFIG, ".xml");
        ResolvedFile resolvedFile = new ResolvedFile(tmpFile);
        String cfgPath = "gs://tradefed_test_resources/configs/test-config.xml";
        try {
            FileUtil.writeToFile(configStream, tmpFile);

            // Inject it into the direct config resolver, then try to load a direct config
            URI cfgUri = new URI(cfgPath);
            Mockito.doReturn(resolvedFile)
                    .when(spyFactory)
                    .resolveRemoteFile(Mockito.eq(cfgUri), Mockito.<URI>any());

            String args[] = {cfgPath, "--null-device"};
            IConfiguration config = spyFactory.createConfigurationFromArgs(args);

            assertNotNull(config);

            // ensure it looks like what we'd expect
            assertTrue(config.getDeviceRequirements().nullDeviceRequested());
            assertEquals(1, config.getTests().size());
            assertTrue(config.getTests().get(0) instanceof StubOptionTest);
        } finally {
            FileUtil.deleteFile(tmpFile);
        }
    }

    private static String getClassName(String name) {
        // -6 because of .class
        return name.substring(0, name.length() - 6).replace('/', '.');
    }

    private Map<String, List<String>> getClassInContribJar() throws IOException {
        Map<String, List<String>> jarToObject = new LinkedHashMap<String, List<String>>();
        for (File jar : getListOfBuiltJars()) {
            List<String> objects = new ArrayList<>();
            JarFile jarFile = null;
            try {
                jarFile = new JarFile(jar);
                Enumeration<JarEntry> e = jarFile.entries();

                while (e.hasMoreElements()) {
                    JarEntry je = e.nextElement();
                    if (je.isDirectory()
                            || !je.getName().endsWith(".class")
                            || je.getName().contains("$")) {
                        continue;
                    }
                    String className = getClassName(je.getName());
                    objects.add(className);
                }
            } finally {
                StreamUtil.close(jarFile);
            }
            jarToObject.put(jar.getName(), objects);
        }
        return jarToObject;
    }

    private List<File> getListOfBuiltJars() {
        // testJarPath is the path of the jar file that contains this test
        // class.  We assume the other jars live in the same dir as this test
        // class' jar.
        String testJarPath =
                ConfigurationFactoryTest.class
                        .getProtectionDomain()
                        .getCodeSource()
                        .getLocation()
                        .getPath();
        File jarFilePath = new File(testJarPath);
        String jarFileParentPath = jarFilePath.getParent();
        List<File> listOfJars = new ArrayList<File>();
        File jarToCheck;
        for (String jar : JAR_TO_CHECK) {
            jarToCheck = new File(jarFileParentPath, jar);
            if (jarToCheck.exists()) {
                listOfJars.add(jarToCheck);
            }
        }
        return listOfJars;
    }
}
