/*
 * 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.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.command.ICommandOptions;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IDeviceRecovery;
import com.android.tradefed.device.IDeviceSelection;
import com.android.tradefed.device.TestDeviceOptions;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.ILeveledLogOutput;
import com.android.tradefed.log.Log.LogLevel;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TextResultReporter;
import com.android.tradefed.targetprep.BaseTargetPreparer;
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IDisableable;

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

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

    private static final String CONFIG_NAME = "name";
    private static final String CONFIG_DESCRIPTION = "config description";
    private static final String CONFIG_OBJECT_TYPE_NAME = "object_name";
    private static final String OPTION_DESCRIPTION = "bool description";
    private static final String OPTION_NAME = "bool";
    private static final String ALT_OPTION_NAME = "map";

    /** Interface for test object stored in a {@link IConfiguration}. */
    private static interface TestConfig {

        public boolean getBool();
    }

    private static class TestConfigObject implements TestConfig, IDisableable {

        @Option(name = OPTION_NAME, description = OPTION_DESCRIPTION, requiredForRerun = true)
        private boolean mBool;

        @Option(name = ALT_OPTION_NAME, description = OPTION_DESCRIPTION)
        private Map<String, Boolean> mBoolMap = new HashMap<String, Boolean>();

        @Option(name = "mandatory-option", mandatory = true)
        private String mMandatory = null;

        private boolean mIsDisabled = false;

        @Override
        public boolean getBool() {
            return mBool;
        }

        public Map<String, Boolean> getMap() {
            return mBoolMap;
        }

        @Override
        public void setDisable(boolean isDisabled) {
            mIsDisabled = isDisabled;
        }

        @Override
        public boolean isDisabled() {
            return mIsDisabled;
        }
    }

    private Configuration mConfig;

    @Before
    public void setUp() throws Exception {
        mConfig = new Configuration(CONFIG_NAME, CONFIG_DESCRIPTION);

        try {
            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
        } catch (IllegalStateException ignored) {
        }
    }

    /**
     * Test that {@link Configuration#getConfigurationObject(String)} can retrieve a previously
     * stored object.
     */
    @Test
    public void testGetConfigurationObject() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        Object fromConfig = mConfig.getConfigurationObject(CONFIG_OBJECT_TYPE_NAME);
        assertEquals(testConfigObject, fromConfig);
    }

    /** Test {@link Configuration#getConfigurationObjectList(String)} */
    @SuppressWarnings("unchecked")
    @Test
    public void testGetConfigurationObjectList() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        List<TestConfig> configList =
                (List<TestConfig>) mConfig.getConfigurationObjectList(CONFIG_OBJECT_TYPE_NAME);
        assertEquals(testConfigObject, configList.get(0));
    }

    /**
     * Test that {@link Configuration#getConfigurationObject(String)} with a name that does not
     * exist.
     */
    @Test
    public void testGetConfigurationObject_wrongname() {
        assertNull(mConfig.getConfigurationObject("non-existent"));
    }

    /**
     * Test that calling {@link Configuration#getConfigurationObject(String)} for a built-in config
     * type that supports lists.
     */
    @Test
    public void testGetConfigurationObject_typeIsList() {
        try {
            mConfig.getConfigurationObject(Configuration.TEST_TYPE_NAME);
            fail("IllegalStateException not thrown");
        } catch (IllegalStateException e) {
            // expected
        }
    }

    /**
     * Test that calling {@link Configuration#getConfigurationObject(String)} for a config type that
     * is a list.
     */
    @Test
    public void testGetConfigurationObject_forList() throws ConfigurationException {
        List<TestConfigObject> list = new ArrayList<TestConfigObject>();
        list.add(new TestConfigObject());
        list.add(new TestConfigObject());
        mConfig.setConfigurationObjectList(CONFIG_OBJECT_TYPE_NAME, list);
        try {
            mConfig.getConfigurationObject(CONFIG_OBJECT_TYPE_NAME);
            fail("IllegalStateException not thrown");
        } catch (IllegalStateException e) {
            // expected
        }
    }

    /**
     * Test that setConfigurationObject throws a ConfigurationException when config object provided
     * is not the correct type
     */
    @Test
    public void testSetConfigurationObject_wrongtype() {
        try {
            // arbitrarily, use the "Test" type as expected type
            mConfig.setConfigurationObject(Configuration.TEST_TYPE_NAME, new TestConfigObject());
            fail("setConfigurationObject did not throw ConfigurationException");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /**
     * Test {@link Configuration#getConfigurationObjectList(String)} when config object with given
     * name does not exist.
     */
    @Test
    public void testGetConfigurationObjectList_wrongname() {
        assertNull(mConfig.getConfigurationObjectList("non-existent"));
    }

    /**
     * Test {@link Configuration#setConfigurationObjectList(String, List)} when config object is the
     * wrong type
     */
    @Test
    public void testSetConfigurationObjectList_wrongtype() {
        try {
            List<TestConfigObject> myList = new ArrayList<TestConfigObject>(1);
            myList.add(new TestConfigObject());
            // arbitrarily, use the "Test" type as expected type
            mConfig.setConfigurationObjectList(Configuration.TEST_TYPE_NAME, myList);
            fail("setConfigurationObject did not throw ConfigurationException");
        } catch (ConfigurationException e) {
            // expected
        }
    }

    /** Test method for {@link Configuration#getBuildProvider()}. */
    @Test
    public void testGetBuildProvider() throws BuildRetrievalError {
        // check that the default provider is present and doesn't blow up
        assertNotNull(mConfig.getBuildProvider().getBuild());
        // check set and get
        final IBuildProvider provider = mock(IBuildProvider.class);
        mConfig.setBuildProvider(provider);
        assertEquals(provider, mConfig.getBuildProvider());
    }

    /** Test method for {@link Configuration#getTargetPreparers()}. */
    @Test
    public void testGetTargetPreparers() throws Exception {
        // check that the callback is working and doesn't blow up
        assertEquals(0, mConfig.getTargetPreparers().size());
        // test set and get
        final ITargetPreparer prep = mock(ITargetPreparer.class);
        mConfig.setTargetPreparer(prep);
        assertEquals(prep, mConfig.getTargetPreparers().get(0));
    }

    /** Test method for {@link Configuration#getTests()}. */
    @Test
    public void testGetTests() throws DeviceNotAvailableException {
        // check that the default test is present and doesn't blow up
        mConfig.getTests()
                .get(0)
                .run(TestInformation.newBuilder().build(), new TextResultReporter());
        IRemoteTest test1 = mock(IRemoteTest.class);
        mConfig.setTest(test1);
        assertEquals(test1, mConfig.getTests().get(0));
    }

    /** Test method for {@link Configuration#getDeviceRecovery()}. */
    @Test
    public void testGetDeviceRecovery() {
        // check that the default recovery is present
        assertNotNull(mConfig.getDeviceRecovery());
        final IDeviceRecovery recovery = mock(IDeviceRecovery.class);
        mConfig.setDeviceRecovery(recovery);
        assertEquals(recovery, mConfig.getDeviceRecovery());
    }

    /** Test method for {@link Configuration#getLogOutput()}. */
    @Test
    public void testGetLogOutput() {
        // check that the default logger is present and doesn't blow up
        mConfig.getLogOutput().printLog(LogLevel.INFO, "testGetLogOutput", "test");
        final ILeveledLogOutput logger = mock(ILeveledLogOutput.class);
        mConfig.setLogOutput(logger);
        assertEquals(logger, mConfig.getLogOutput());
    }

    /**
     * Test method for {@link Configuration#getTestInvocationListeners()}.
     *
     * @throws ConfigurationException
     */
    @Test
    public void testGetTestInvocationListeners() throws ConfigurationException {
        // check that the default listener is present and doesn't blow up
        ITestInvocationListener defaultListener = mConfig.getTestInvocationListeners().get(0);
        defaultListener.invocationStarted(new InvocationContext());
        defaultListener.invocationEnded(1);

        final ITestInvocationListener listener1 = mock(ITestInvocationListener.class);
        mConfig.setTestInvocationListener(listener1);
        assertEquals(listener1, mConfig.getTestInvocationListeners().get(0));
    }

    /** Test method for {@link Configuration#getCommandOptions()}. */
    @Test
    public void testGetCommandOptions() {
        // check that the default object is present
        assertNotNull(mConfig.getCommandOptions());
        final ICommandOptions cmdOptions = mock(ICommandOptions.class);
        mConfig.setCommandOptions(cmdOptions);
        assertEquals(cmdOptions, mConfig.getCommandOptions());
    }

    /** Test method for {@link Configuration#getDeviceRequirements()}. */
    @Test
    public void testGetDeviceRequirements() {
        // check that the default object is present
        assertNotNull(mConfig.getDeviceRequirements());
        final IDeviceSelection deviceSelection = mock(IDeviceSelection.class);
        mConfig.setDeviceRequirements(deviceSelection);
        assertEquals(deviceSelection, mConfig.getDeviceRequirements());
    }

    /**
     * Test {@link Configuration#setConfigurationObject(String, Object)} with a {@link
     * IConfigurationReceiver}
     */
    @Test
    public void testSetConfigurationObject_configReceiver() throws ConfigurationException {
        final IConfigurationReceiver mockConfigReceiver = mock(IConfigurationReceiver.class);

        mConfig.setConfigurationObject("example", mockConfigReceiver);

        verify(mockConfigReceiver).setConfiguration(mConfig);
    }

    /** Test {@link Configuration#injectOptionValue(String, String)} */
    @Test
    public void testInjectOptionValue() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        mConfig.injectOptionValue(OPTION_NAME, Boolean.toString(true));
        assertTrue(testConfigObject.getBool());
        assertEquals(1, mConfig.getConfigurationDescription().getRerunOptions().size());
        OptionDef optionDef = mConfig.getConfigurationDescription().getRerunOptions().get(0);
        assertEquals(OPTION_NAME, optionDef.name);
    }

    /** Test {@link Configuration#injectOptionValue(String, String, String)} */
    @Test
    public void testInjectMapOptionValue() throws ConfigurationException {
        final String key = "hello";

        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        assertEquals(0, testConfigObject.getMap().size());
        mConfig.injectOptionValue(ALT_OPTION_NAME, key, Boolean.toString(true));

        Map<String, Boolean> map = testConfigObject.getMap();
        assertEquals(1, map.size());
        assertNotNull(map.get(key));
        assertTrue(map.get(key).booleanValue());
    }

    /**
     * Test {@link Configuration#injectOptionValue(String, String)} is throwing an exception for map
     * options without no map key provided in the option value
     */
    @Test
    public void testInjectParsedMapOptionValueNoKey() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        assertEquals(0, testConfigObject.getMap().size());

        try {
            mConfig.injectOptionValue(ALT_OPTION_NAME, "wrong_value");
            fail("ConfigurationException is not thrown for a map option without retrievable key");
        } catch (ConfigurationException ignore) {
            // expected
        }
    }

    /**
     * Test {@link Configuration#injectOptionValue(String, String)} is throwing an exception for map
     * options with ambiguous map key provided in the option value (multiple equal signs)
     */
    @Test
    public void testInjectParsedMapOptionValueAmbiguousKey() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        assertEquals(0, testConfigObject.getMap().size());

        try {
            mConfig.injectOptionValue(ALT_OPTION_NAME, "a=b=c");
            fail("ConfigurationException is not thrown for a map option with ambiguous key");
        } catch (ConfigurationException ignore) {
            // expected
        }
    }

    /**
     * Test {@link Configuration#injectOptionValue(String, String)} is correctly parsing map options
     */
    @Test
    public void testInjectParsedMapOptionValue() throws ConfigurationException {
        final String key = "hello\\=key";

        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        assertEquals(0, testConfigObject.getMap().size());
        mConfig.injectOptionValue(ALT_OPTION_NAME, key + "=" + Boolean.toString(true));

        Map<String, Boolean> map = testConfigObject.getMap();
        assertEquals(1, map.size());
        assertNotNull(map.get(key));
        assertTrue(map.get(key));
    }

    /** Test {@link Configuration#injectOptionValues(List)} */
    @Test
    public void testInjectOptionValues() throws ConfigurationException {
        final String key = "hello";
        List<OptionDef> options = new ArrayList<>();
        options.add(new OptionDef(OPTION_NAME, Boolean.toString(true), null));
        options.add(new OptionDef(ALT_OPTION_NAME, key, Boolean.toString(true), null));

        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        mConfig.injectOptionValues(options);

        assertTrue(testConfigObject.getBool());
        Map<String, Boolean> map = testConfigObject.getMap();
        assertEquals(1, map.size());
        assertNotNull(map.get(key));
        assertTrue(map.get(key).booleanValue());
        assertEquals(1, mConfig.getConfigurationDescription().getRerunOptions().size());
        OptionDef optionDef = mConfig.getConfigurationDescription().getRerunOptions().get(0);
        assertEquals(OPTION_NAME, optionDef.name);
    }

    /** Basic test for {@link Configuration#printCommandUsage(boolean, java.io.PrintStream)}. */
    @Test
    public void testPrintCommandUsage() throws ConfigurationException {
        TestConfigObject testConfigObject = new TestConfigObject();
        mConfig.setConfigurationObject(CONFIG_OBJECT_TYPE_NAME, testConfigObject);
        // dump the print stream results to the ByteArrayOutputStream, so contents can be evaluated
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PrintStream mockPrintStream = new PrintStream(outputStream);
        mConfig.printCommandUsage(false, mockPrintStream);

        // verifying exact contents would be prone to high-maintenance, so instead, just validate
        // all expected names are present
        final String usageString = outputStream.toString();
        assertTrue("Usage text does not contain config name", usageString.contains(CONFIG_NAME));
        assertTrue(
                "Usage text does not contain config description",
                usageString.contains(CONFIG_DESCRIPTION));
        assertTrue(
                "Usage text does not contain object name",
                usageString.contains(CONFIG_OBJECT_TYPE_NAME));
        assertTrue("Usage text does not contain option name", usageString.contains(OPTION_NAME));
        assertTrue(
                "Usage text does not contain option description",
                usageString.contains(OPTION_DESCRIPTION));

        // ensure help prints out options from default config types
        assertTrue(
                "Usage text does not contain --serial option name", usageString.contains("serial"));
    }

    /**
     * Test that {@link Configuration#validateOptions()} doesn't throw when all mandatory fields are
     * set.
     */
    @Test
    public void testValidateOptions() throws ConfigurationException {
        mConfig.validateOptions();
    }

    /**
     * Test that {@link Configuration#validateOptions()} throw when all mandatory fields are not set
     * and object is not disabled.
     */
    @Test
    public void testValidateOptions_nonDisabledObject() throws ConfigurationException {
        TestConfigObject object = new TestConfigObject();
        object.setDisable(false);
        mConfig.setConfigurationObject("helper", object);
        try {
            mConfig.validateOptions();
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertTrue(expected.getMessage().contains("Found missing mandatory options"));
        }
    }

    /**
     * Test that {@link Configuration#validateOptions()} doesn't throw when all mandatory fields are
     * not set but the object is disabled.
     */
    @Test
    public void testValidateOptions_disabledObject() throws ConfigurationException {
        TestConfigObject object = new TestConfigObject();
        object.setDisable(true);
        mConfig.setConfigurationObject("helper", object);
        mConfig.validateOptions();
    }

    /**
     * Test that {@link Configuration#validateOptions()} throws a config exception when shard count
     * is negative number.
     */
    @Test
    public void testValidateOptionsShardException() throws ConfigurationException {
        ICommandOptions option =
                new CommandOptions() {
                    @Override
                    public Integer getShardCount() {
                        return -1;
                    }
                };
        mConfig.setConfigurationObject(Configuration.CMD_OPTIONS_TYPE_NAME, option);
        try {
            mConfig.validateOptions();
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertEquals("a shard count must be a positive number", expected.getMessage());
        }
    }

    /**
     * Test that {@link Configuration#validateOptions()} throws a config exception when shard index
     * is not valid.
     */
    @Test
    public void testValidateOptionsShardIndexException() throws ConfigurationException {
        ICommandOptions option =
                new CommandOptions() {
                    @Override
                    public Integer getShardIndex() {
                        return -1;
                    }
                };
        mConfig.setConfigurationObject(Configuration.CMD_OPTIONS_TYPE_NAME, option);
        try {
            mConfig.validateOptions();
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertEquals("a shard index must be in range [0, shard count)", expected.getMessage());
        }
    }

    /**
     * Test that {@link Configuration#validateOptions()} throws a config exception when shard index
     * is above the shard count.
     */
    @Test
    public void testValidateOptionsShardIndexAboveShardCount() throws ConfigurationException {
        ICommandOptions option =
                new CommandOptions() {
                    @Override
                    public Integer getShardIndex() {
                        return 3;
                    }

                    @Override
                    public Integer getShardCount() {
                        return 2;
                    }
                };
        mConfig.setConfigurationObject(Configuration.CMD_OPTIONS_TYPE_NAME, option);
        try {
            mConfig.validateOptions();
            fail("Should have thrown an exception.");
        } catch (ConfigurationException expected) {
            assertEquals("a shard index must be in range [0, shard count)", expected.getMessage());
        }
    }

    /**
     * Ensure that dynamic file download is not triggered in the parent invocation of local
     * sharding. If that was the case, the downloaded files would be cleaned up right after the
     * shards are kicked-off in new invocations.
     */
    @Test
    public void testValidateOptions_localSharding_skipDownload() throws Exception {
        mConfig =
                new Configuration(CONFIG_NAME, CONFIG_DESCRIPTION) {
                    @Override
                    protected boolean isRemoteEnvironment() {
                        return false;
                    }
                };
        CommandOptions options = new CommandOptions();
        options.setShardCount(5);
        options.setShardIndex(null);
        mConfig.setCommandOptions(options);
        TestDeviceOptions deviceOptions = new TestDeviceOptions();
        File fakeConfigFile = new File("gs://bucket/remote/file/path");
        deviceOptions.setAvdConfigFile(fakeConfigFile);
        mConfig.setDeviceOptions(deviceOptions);

        // No exception for download is thrown because no download occurred.
        mConfig.validateOptions();
        mConfig.resolveDynamicOptions(new DynamicRemoteFileResolver());
        // Dynamic file is not resolved.
        assertEquals(fakeConfigFile, deviceOptions.getAvdConfigFile());
    }

    /** Test that {@link Configuration#dumpXml(PrintWriter)} produce the xml output. */
    @Test
    public void testDumpXml() throws IOException {
        File test = FileUtil.createTempFile("dumpxml", "xml");
        try {
            PrintWriter out = new PrintWriter(test);
            mConfig.dumpXml(out);
            out.flush();
            String content = FileUtil.readStringFromFile(test);
            assertTrue(content.length() > 100);
            assertTrue(content.contains("<configuration>"));
            assertTrue(content.contains("<test class"));
        } finally {
            FileUtil.deleteFile(test);
        }
    }

    /**
     * Test that {@link Configuration#dumpXml(PrintWriter)} produce the xml output without objects
     * that have been filtered.
     */
    @Test
    public void testDumpXml_withFilter() throws IOException {
        File test = FileUtil.createTempFile("dumpxml", "xml");
        try {
            PrintWriter out = new PrintWriter(test);
            List<String> filters = new ArrayList<>();
            filters.add(Configuration.TEST_TYPE_NAME);
            mConfig.dumpXml(out, filters);
            out.flush();
            String content = FileUtil.readStringFromFile(test);
            assertTrue(content.length() > 100);
            assertTrue(content.contains("<configuration>"));
            assertFalse(content.contains("<test class"));
        } finally {
            FileUtil.deleteFile(test);
        }
    }

    /**
     * Test that {@link Configuration#dumpXml(PrintWriter)} produce the xml output even for a multi
     * device situation.
     */
    @Test
    public void testDumpXml_multi_device() throws Exception {
        List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
        deviceObjectList.add(new DeviceConfigurationHolder("device1"));
        deviceObjectList.add(new DeviceConfigurationHolder("device2"));
        mConfig.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
        File test = FileUtil.createTempFile("dumpxml", "xml");
        try {
            PrintWriter out = new PrintWriter(test);
            mConfig.dumpXml(out);
            out.flush();
            String content = FileUtil.readStringFromFile(test);
            assertTrue(content.length() > 100);
            assertTrue(content.contains("<device name=\"device1\">"));
            assertTrue(content.contains("<device name=\"device2\">"));
        } finally {
            FileUtil.deleteFile(test);
        }
    }

    /**
     * Test that {@link Configuration#dumpXml(PrintWriter)} produce the xml output even for a multi
     * device situation when one of the device is fake.
     */
    @Test
    public void testDumpXml_multi_device_fake() throws Exception {
        List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
        deviceObjectList.add(new DeviceConfigurationHolder("device1", true));
        deviceObjectList.add(new DeviceConfigurationHolder("device2"));
        mConfig.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
        File test = FileUtil.createTempFile("dumpxml", "xml");
        try {
            PrintWriter out = new PrintWriter(test);
            mConfig.dumpXml(out);
            out.flush();
            String content = FileUtil.readStringFromFile(test);
            assertTrue(content.length() > 100);
            assertTrue(content.contains("<device name=\"device1\" isFake=\"true\">"));
            assertTrue(content.contains("<device name=\"device2\">"));
        } finally {
            FileUtil.deleteFile(test);
        }
    }

    /** Ensure that the dump xml only considere trully changed option on the same object. */
    @Test
    public void testDumpChangedOption() throws Exception {
        CommandOptions options1 = new CommandOptions();
        Configuration one = new Configuration("test", "test");
        one.setCommandOptions(options1);
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        one.dumpXml(pw, new ArrayList<>(), true, false);
        String noOption = sw.toString();
        assertTrue(
                noOption.contains(
                        "<cmd_options class=\"com.android.tradefed.command.CommandOptions\" />"));

        OptionSetter setter = new OptionSetter(options1);
        setter.setOptionValue("test-tag", "tag-value");
        sw = new StringWriter();
        pw = new PrintWriter(sw);
        one.dumpXml(pw, new ArrayList<>(), true, false);
        String withOption = sw.toString();
        assertTrue(withOption.contains("<option name=\"test-tag\" value=\"tag-value\" />"));

        CommandOptions options2 = new CommandOptions();
        one.setCommandOptions(options2);
        sw = new StringWriter();
        pw = new PrintWriter(sw);
        one.dumpXml(pw, new ArrayList<>(), true, false);
        String differentObject = sw.toString();
        assertTrue(
                differentObject.contains(
                        "<cmd_options class=\"com.android.tradefed.command.CommandOptions\" />"));
    }

    /** Ensure we print modified option if they are structures. */
    @Test
    public void testDumpChangedOption_structure() throws Exception {
        CommandOptions options1 = new CommandOptions();
        Configuration one = new Configuration("test", "test");
        one.setCommandOptions(options1);
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        one.dumpXml(pw, new ArrayList<>(), true, false);
        String noOption = sw.toString();
        assertTrue(
                noOption.contains(
                        "<cmd_options class=\"com.android.tradefed.command.CommandOptions\" />"));

        OptionSetter setter = new OptionSetter(options1);
        setter.setOptionValue("invocation-data", "key", "value");
        setter.setOptionValue("auto-collect", "LOGCAT_ON_FAILURE");
        sw = new StringWriter();
        pw = new PrintWriter(sw);
        one.dumpXml(pw, new ArrayList<>(), true, false);
        String withOption = sw.toString();
        assertTrue(
                withOption.contains(
                        "<option name=\"invocation-data\" key=\"key\" value=\"value\" />"));
        assertTrue(
                withOption.contains(
                        "<option name=\"auto-collect\" value=\"LOGCAT_ON_FAILURE\" />"));
    }

    @Test
    public void testDeepClone() throws Exception {
        Configuration original =
                (Configuration)
                        ConfigurationFactory.getInstance()
                                .createConfigurationFromArgs(
                                        new String[] {"instrumentations"}, null, null);
        IConfiguration copy =
                original.partialDeepClone(
                        Arrays.asList(Configuration.DEVICE_NAME, Configuration.TEST_TYPE_NAME),
                        null);
        assertNotEquals(
                original.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME),
                copy.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME));
        assertNotEquals(original.getTargetPreparers().get(0), copy.getTargetPreparers().get(0));
        assertNotEquals(
                original.getDeviceConfig().get(0).getTargetPreparers().get(0),
                copy.getDeviceConfig().get(0).getTargetPreparers().get(0));
        assertNotEquals(original.getTests().get(0), copy.getTests().get(0));
        copy.validateOptions();
    }

    @Test
    public void testDeepClone_innerDevice() throws Exception {
        Configuration original =
                (Configuration)
                        ConfigurationFactory.getInstance()
                                .createConfigurationFromArgs(
                                        new String[] {"instrumentations"}, null, null);
        IConfiguration copy =
                original.partialDeepClone(
                        Arrays.asList(
                                Configuration.TARGET_PREPARER_TYPE_NAME,
                                Configuration.TEST_TYPE_NAME),
                        null);
        assertNotEquals(
                original.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME),
                copy.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME));
        assertNotEquals(original.getTargetPreparers().get(0), copy.getTargetPreparers().get(0));
        assertNotEquals(
                original.getDeviceConfig().get(0).getTargetPreparers().get(0),
                copy.getDeviceConfig().get(0).getTargetPreparers().get(0));
        assertNotEquals(original.getTests().get(0), copy.getTests().get(0));
        copy.validateOptions();
    }

    @Test
    public void testDeepClone_configReceiver() throws Exception {
        Configuration original = new Configuration("test", "test");
        ConfigReceiverPreparer oriReceiver = new ConfigReceiverPreparer();
        oriReceiver.setConfiguration(original);
        original.setTargetPreparer(oriReceiver);
        assertNotNull(oriReceiver.getConfiguration());

        IConfiguration copy =
                original.partialDeepClone(
                        Arrays.asList(Configuration.TARGET_PREPARER_TYPE_NAME), null);
        assertNotEquals(
                original.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME),
                copy.getDeviceConfigByName(ConfigurationDef.DEFAULT_DEVICE_NAME));
        assertNotEquals(original.getTargetPreparers().get(0), copy.getTargetPreparers().get(0));
        ConfigReceiverPreparer copyReceiver =
                (ConfigReceiverPreparer) copy.getTargetPreparers().get(0);
        assertNotNull(copyReceiver.getConfiguration());
    }

    public static class ConfigReceiverPreparer extends BaseTargetPreparer
            implements IConfigurationReceiver {

        private IConfiguration mConfig;

        @Override
        public void setConfiguration(IConfiguration configuration) {
            mConfig = configuration;
        }

        public IConfiguration getConfiguration() {
            return mConfig;
        }
    }
}
