diff --git a/core/Config.php b/core/Config.php
index f0b5c3ed264d09f97313bab6b586f827776df57e..91853de73380cdbac343ef02b40659071a765ab7 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -10,10 +10,9 @@
 namespace Piwik;
 
 use Exception;
+use Piwik\Config\IniFileChain;
 use Piwik\Config\ConfigNotFoundException;
-use Piwik\Ini\IniReader;
 use Piwik\Ini\IniReadingException;
-use Piwik\Ini\IniWriter;
 
 /**
  * Singleton that provides read & write access to Piwik's INI configuration.
@@ -51,11 +50,6 @@ class Config extends Singleton
     /**
      * @var boolean
      */
-    protected $initialized = false;
-    protected $configGlobal = array();
-    protected $configLocal = array();
-    protected $configCommon = array();
-    protected $configCache = array();
     protected $pathGlobal = null;
     protected $pathCommon = null;
     protected $pathLocal = null;
@@ -66,22 +60,19 @@ class Config extends Singleton
     protected $doNotWriteConfigInTests = false;
 
     /**
-     * @var IniReader
+     * @var IniFileChain
      */
-    private $iniReader;
+    protected $settings;
 
-    /**
-     * @var IniWriter
-     */
-    private $iniWriter;
+    private $initialized = false;
 
     public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
     {
         $this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath();
         $this->pathCommon = $pathCommon ?: self::getCommonConfigPath();
         $this->pathLocal = $pathLocal ?: self::getLocalConfigPath();
-        $this->iniReader = new IniReader();
-        $this->iniWriter = new IniWriter();
+
+        $this->settings = new IniFileChain();
     }
 
     /**
@@ -131,30 +122,28 @@ class Config extends Singleton
         $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
         $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
 
-        $this->init();
+        $this->reload();
 
-        if (isset($this->configGlobal['database_tests'])
-            || isset($this->configLocal['database_tests'])
-        ) {
-            $this->__get('database_tests');
-            $this->configCache['database'] = $this->configCache['database_tests'];
+        $databaseTestsSettings = $this->database_tests;
+        if (!empty($databaseTestsSettings)) {
+            $this->database = $databaseTestsSettings;
         }
 
         // Ensure local mods do not affect tests
         if (empty($pathGlobal)) {
-            $this->configCache['Debug'] = $this->configGlobal['Debug'];
-            $this->configCache['mail'] = $this->configGlobal['mail'];
-            $this->configCache['General'] = $this->configGlobal['General'];
-            $this->configCache['Segments'] = $this->configGlobal['Segments'];
-            $this->configCache['Tracker'] = $this->configGlobal['Tracker'];
-            $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs'];
-            $this->configCache['Deletereports'] = $this->configGlobal['Deletereports'];
-            $this->configCache['Development'] = $this->configGlobal['Development'];
+            $this->Debug = $this->settings->getFrom($this->pathGlobal, 'Debug');
+            $this->mail = $this->settings->getFrom($this->pathGlobal, 'mail');
+            $this->General = $this->settings->getFrom($this->pathGlobal, 'General');
+            $this->Segments = $this->settings->getFrom($this->pathGlobal, 'Segments');
+            $this->Tracker = $this->settings->getFrom($this->pathGlobal, 'Tracker');
+            $this->Deletelogs = $this->settings->getFrom($this->pathGlobal, 'Deletelogs');
+            $this->Deletereports = $this->settings->getFrom($this->pathGlobal, 'Deletereports');
+            $this->Development = $this->settings->getFrom($this->pathGlobal, 'Development');
         }
 
         // for unit tests, we set that no plugin is installed. This will force
         // the test initialization to create the plugins tables, execute ALTER queries, etc.
-        $this->configCache['PluginsInstalled'] = array('PluginsInstalled' => array());
+        $this->PluginsInstalled = array('PluginsInstalled' => array());
     }
 
     /**
@@ -301,59 +290,56 @@ class Config extends Singleton
 
     /**
      * Clear in-memory configuration so it can be reloaded
+     * @deprecated since v2.12.0
      */
     public function clear()
     {
-        $this->configGlobal = array();
-        $this->configLocal = array();
-        $this->configCommon = array();
-        $this->configCache = array();
         $this->initialized = false;
+        $this->reload();
     }
 
     /**
      * Read configuration from files into memory
      *
      * @throws Exception if local config file is not readable; exits for other errors
+     * @deprecated since v2.12.0
      */
     public function init()
     {
-        $this->clear();
+        $this->reload();
+    }
+
+    /**
+     * Reloads config data from disk.
+     *
+     * @throws \Exception if the global config file is not found and this is a tracker request, or
+     *                    if the local config file is not found and this is NOT a tracker request.
+     */
+    public function reload()
+    {
         $this->initialized = true;
-        $reportError = SettingsServer::isTrackerApiRequest();
+
+        $inTrackerRequest = SettingsServer::isTrackerApiRequest();
 
         // read defaults from global.ini.php
-        if (!is_readable($this->pathGlobal) && $reportError) {
+        if (!is_readable($this->pathGlobal) && $inTrackerRequest) {
             throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal)));
         }
 
         try {
-            $this->configGlobal = $this->iniReader->readFile($this->pathGlobal);
+            $this->settings->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal);
         } catch (IniReadingException $e) {
-            if ($reportError) {
-                throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
+            if ($inTrackerRequest) {
+                throw $e;
             }
         }
 
-        try {
-            if (file_exists($this->pathCommon)) {
-                $this->configCommon = $this->iniReader->readFile($this->pathCommon);
-            } else {
-                $this->configCommon = false;
-            }
-        } catch (IniReadingException $e) {
-            $this->configCommon = false;
-        }
+        // decode section datas
+        $this->decodeValues($this->settings->getAll());
 
         // Check config.ini.php last
-        $this->checkLocalConfigFound();
-
-        try {
-            $this->configLocal = $this->iniReader->readFile($this->pathLocal);
-        } catch (IniReadingException $e) {
-            if ($reportError) {
-                throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
-            }
+        if (!$inTrackerRequest) {
+            $this->checkLocalConfigFound();
         }
     }
 
@@ -381,7 +367,7 @@ class Config extends Singleton
      * @param mixed $values
      * @return mixed
      */
-    protected function decodeValues($values)
+    protected function decodeValues(&$values)
     {
         if (is_array($values)) {
             foreach ($values as &$value) {
@@ -400,7 +386,7 @@ class Config extends Singleton
      * @param mixed $values
      * @return mixed
      */
-    protected function encodeValues($values)
+    protected function encodeValues(&$values)
     {
         if (is_array($values)) {
             foreach ($values as &$value) {
@@ -427,60 +413,26 @@ class Config extends Singleton
     public function &__get($name)
     {
         if (!$this->initialized) {
-            $this->init();
+            $this->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal);
 
             // must be called here, not in init(), since setTestEnvironment() calls init(). (this avoids
             // infinite recursion)
-            Piwik::postTestEvent('Config.createConfigSingleton',
-                array($this, &$this->configCache, &$this->configLocal));
-        }
-
-        // check cache for merged section
-        if (isset($this->configCache[$name])) {
-            $tmp =& $this->configCache[$name];
-            return $tmp;
-        }
-
-        $section = $this->getFromGlobalConfig($name);
-        $sectionCommon = $this->getFromCommonConfig($name);
-        if (empty($section) && !empty($sectionCommon)) {
-            $section = $sectionCommon;
-        } elseif (!empty($section) && !empty($sectionCommon)) {
-            $section = $this->array_merge_recursive_distinct($section, $sectionCommon);
+            $allSettings =& $this->settings->getAll();
+            Piwik::postTestEvent('Config.createConfigSingleton', array($this, &$allSettings));
         }
 
-        if (isset($this->configLocal[$name])) {
-            // local settings override the global defaults
-            $section = $section
-                ? array_merge($section, $this->configLocal[$name])
-                : $this->configLocal[$name];
-        }
-
-        if ($section === null) {
-            $section = array();
-        }
-
-        // cache merged section for later
-        $this->configCache[$name] = $this->decodeValues($section);
-        $tmp =& $this->configCache[$name];
-
-        return $tmp;
+        $section =& $this->settings->get($name);
+        return $section;
     }
 
     public function getFromGlobalConfig($name)
     {
-        if (isset($this->configGlobal[$name])) {
-            return $this->configGlobal[$name];
-        }
-        return null;
+        return $this->settings->getFrom($this->pathGlobal, $name);
     }
 
     public function getFromCommonConfig($name)
     {
-        if (isset($this->configCommon[$name])) {
-            return $this->configCommon[$name];
-        }
-        return null;
+        return $this->settings->getFrom($this->pathCommon, $name);
     }
 
     /**
@@ -492,125 +444,32 @@ class Config extends Singleton
      */
     public function __set($name, $value)
     {
-        $this->configCache[$name] = $value;
-    }
-
-    /**
-     * Comparison function
-     *
-     * @param mixed $elem1
-     * @param mixed $elem2
-     * @return int;
-     */
-    public static function compareElements($elem1, $elem2)
-    {
-        if (is_array($elem1)) {
-            if (is_array($elem2)) {
-                return strcmp(serialize($elem1), serialize($elem2));
-            }
-
-            return 1;
-        }
-
-        if (is_array($elem2)) {
-            return -1;
-        }
-
-        if ((string)$elem1 === (string)$elem2) {
-            return 0;
-        }
-
-        return ((string)$elem1 > (string)$elem2) ? 1 : -1;
-    }
-
-    /**
-     * Compare arrays and return difference, such that:
-     *
-     *     $modified = array_merge($original, $difference);
-     *
-     * @param array $original original array
-     * @param array $modified modified array
-     * @return array differences between original and modified
-     */
-    public function array_unmerge($original, $modified)
-    {
-        // return key/value pairs for keys in $modified but not in $original
-        // return key/value pairs for keys in both $modified and $original, but values differ
-        // ignore keys that are in $original but not in $modified
-
-        return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements'));
+        $this->settings->set($name, $value);
     }
 
     /**
      * Dump config
      *
-     * @param array $configLocal
-     * @param array $configGlobal
-     * @param array $configCommon
-     * @param array $configCache
-     * @return string
+     * @return string|null
+     * @throws \Exception
      */
-    public function dumpConfig($configLocal, $configGlobal, $configCommon, $configCache)
+    public function dumpConfig()
     {
-        $dirty = false;
-
-        if (!$configCache) {
-            return false;
-        }
-
-        $configToWrite = array();
-
-        // If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values
-        if (!empty($configCommon)) {
-            $configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon);
-        }
-
-        if ($configLocal) {
-            foreach ($configLocal as $name => $section) {
-                if (!isset($configCache[$name])) {
-                    $configCache[$name] = $this->decodeValues($section);
-                }
-            }
-        }
-
-        $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache)));
-
-        foreach ($sectionNames as $section) {
-            if (!isset($configCache[$section])) {
-                continue;
-            }
+        $this->encodeValues($this->settings->getAll());
 
-            // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php)
-
-            // get local and cached config
-            $local = isset($configLocal[$section]) ? $configLocal[$section] : array();
-            $config = $configCache[$section];
-
-            // remove default values from both (they should not get written to local)
-            if (isset($configGlobal[$section])) {
-                $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]);
-                $local = $this->array_unmerge($configGlobal[$section], $local);
-            }
+        try {
+            $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
+            $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
+            $dumpedString = $this->settings->dumpChanges($header);
 
-            // if either local/config have non-default values and the other doesn't,
-            // OR both have values, but different values, we must write to config.ini.php
-            if (empty($local) xor empty($config)
-                || (!empty($local)
-                    && !empty($config)
-                    && self::compareElements($config, $configLocal[$section]))
-            ) {
-                $dirty = true;
-            }
+            $this->decodeValues($this->settings->getAll());
+        } catch (Exception $ex) {
+            $this->decodeValues($this->settings->getAll());
 
-            $configToWrite[$section] = array_map(array($this, 'encodeValues'), $config);
+            throw $ex;
         }
 
-        if ($dirty) {
-            $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
-            $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
-            return $this->iniWriter->writeToString($configToWrite, $header);
-        }
-        return false;
+        return $dumpedString;
     }
 
     /**
@@ -625,22 +484,24 @@ class Config extends Singleton
      *
      * @throws \Exception if config file not writable
      */
-    protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true)
+    protected function writeConfig($clear = true)
     {
         if ($this->doNotWriteConfigInTests) {
             return;
         }
 
-        $output = $this->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache);
-        if ($output !== false) {
-            $success = @file_put_contents($pathLocal, $output);
-            if (!$success) {
+        $output = $this->dumpConfig();
+        if ($output !== null
+            && $output !== false
+        ) {
+            $success = @file_put_contents($this->pathLocal, $output);
+            if ($success === false) {
                 throw $this->getConfigNotWritableException();
             }
         }
 
         if ($clear) {
-            $this->clear();
+            $this->reload();
         }
     }
 
@@ -652,7 +513,7 @@ class Config extends Singleton
      */
     public function forceSave()
     {
-        $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCommon, $this->configCache, $this->pathLocal);
+        $this->writeConfig();
     }
 
     /**
@@ -663,42 +524,4 @@ class Config extends Singleton
         $path = "config/" . basename($this->pathLocal);
         return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", "")));
     }
-
-    /**
-     * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
-     * keys to arrays rather than overwriting the value in the first array with the duplicate
-     * value in the second array, as array_merge does. I.e., with array_merge_recursive,
-     * this happens (documented behavior):
-     *
-     * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
-     *     => array('key' => array('org value', 'new value'));
-     *
-     * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
-     * Matching keys' values in the second array overwrite those in the first array, as is the
-     * case with array_merge, i.e.:
-     *
-     * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
-     *     => array('key' => array('new value'));
-     *
-     * Parameters are passed by reference, though only for performance reasons. They're not
-     * altered by this function.
-     *
-     * @param array $array1
-     * @param array $array2
-     * @return array
-     * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
-     * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
-     */
-    function array_merge_recursive_distinct ( array &$array1, array &$array2 )
-    {
-        $merged = $array1;
-        foreach ( $array2 as $key => &$value ) {
-            if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) {
-                $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value );
-            } else {
-                $merged [$key] = $value;
-            }
-        }
-        return $merged;
-    }
 }
\ No newline at end of file
diff --git a/core/Config/IniFileChain.php b/core/Config/IniFileChain.php
new file mode 100644
index 0000000000000000000000000000000000000000..247f97c30969dbe077835a43bcf24629ca37fdb2
--- /dev/null
+++ b/core/Config/IniFileChain.php
@@ -0,0 +1,370 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Config;
+
+use Piwik\Ini\IniReader;
+use Piwik\Ini\IniReadingException;
+use Piwik\Ini\IniWriter;
+use Piwik\Piwik;
+
+/**
+ * Manages a list of INI files where the settings in each INI file merge with or override the
+ * settings in the previous INI file.
+ *
+ * The IniFileChain class manages two types of INI files: multiple default setting files and one
+ * user settings file.
+ *
+ * The default setting files (for example, global.ini.php & common.ini.php) hold the default setting values.
+ * The settings in these files are merged recursively, however, array settings in one file will still
+ * overwrite settings in the previous file.
+ *
+ * Default settings files cannot be modified through the IniFileChain class.
+ *
+ * The user settings file (for example, config.ini.php) holds the actual setting values. Settings in the
+ * user settings files overwrite other settings. So array settings will not merge w/ previous values.
+ */
+class IniFileChain
+{
+    /**
+     * Maps INI file names with their parsed contents. The order of the files signifies the order
+     * in the chain. Files with lower index are overwritten/merged with files w/ a higher index.
+     *
+     * @var array
+     */
+    protected $settingsChain = array();
+
+    /**
+     * The merged INI settings.
+     *
+     * @var array
+     */
+    protected $mergedSettings = array();
+
+    /**
+     * Constructor.
+     *
+     * @param string[] $defaultSettingsFiles The list of paths to INI files w/ the default setting values.
+     * @param string|null $userSettingsFile The path to the user settings file.
+     */
+    public function __construct(array $defaultSettingsFiles = array(), $userSettingsFile = null)
+    {
+        $this->reload($defaultSettingsFiles, $userSettingsFile);
+    }
+
+    /**
+     * Return setting section by reference.
+     *
+     * @param string $name
+     * @return mixed
+     */
+    public function &get($name)
+    {
+        if (!isset($this->mergedSettings[$name])) {
+            $this->mergedSettings[$name] = array();
+        }
+
+        $result =& $this->mergedSettings[$name];
+        return $result;
+    }
+
+    /**
+     * Return setting section from a specific file, rather than the current merged settings.
+     *
+     * @param string $file The path of the file. Should be the path used in construction or reload().
+     * @param string $name The name of the section to access.
+     */
+    public function getFrom($file, $name)
+    {
+        return @$this->settingsChain[$file][$name];
+    }
+
+    /**
+     * Sets a setting value.
+     *
+     * @param string $name
+     * @param mixed $value
+     */
+    public function set($name, $value)
+    {
+        $this->mergedSettings[$name] = $value;
+    }
+
+    /**
+     * Returns all settings. Changes made to the array result will be reflected in the
+     * IniFileChain instance.
+     *
+     * @return array
+     */
+    public function &getAll()
+    {
+        return $this->mergedSettings;
+    }
+
+    /**
+     * Dumps the current in-memory setting values to a string in INI format and returns it.
+     *
+     * @param string $header The header of the output INI file.
+     * @return string The dumped INI contents.
+     */
+    public function dump($header = '')
+    {
+        $writer = new IniWriter();
+        return $writer->writeToString($this->mergedSettings, $header);
+    }
+
+    /**
+     * Writes the difference of the in-memory setting values and the on-disk user settings file setting
+     * values to a string in INI format, and returns it.
+     *
+     * If a config section is identical to the default settings section (as computed by merging
+     * all default setting files), it is not written to the user settings file.
+     *
+     * @param string $header The header of the INI output.
+     * @return string The dumped INI contents.
+     */
+    public function dumpChanges($header = '')
+    {
+        $userSettingsFile = $this->getUserSettingsFile();
+
+        $defaultSettings = $this->getMergedDefaultSettings();
+        $existingMutableSettings = $this->settingsChain[$userSettingsFile];
+
+        $dirty = false;
+
+        $configToWrite = array();
+        foreach ($this->mergedSettings as $sectionName => $changedSection) {
+            $existingMutableSection = @$existingMutableSettings[$sectionName] ?: array();
+
+            // remove default values from both (they should not get written to local)
+            if (isset($defaultSettings[$sectionName])) {
+                $changedSection = $this->arrayUnmerge($defaultSettings[$sectionName], $changedSection);
+                $existingMutableSection = $this->arrayUnmerge($defaultSettings[$sectionName], $existingMutableSection);
+            }
+
+            // if either local/config have non-default values and the other doesn't,
+            // OR both have values, but different values, we must write to config.ini.php
+            if (empty($changedSection) xor empty($existingMutableSection)
+                || (!empty($changedSection)
+                    && !empty($existingMutableSection)
+                    && self::compareElements($changedSection, $existingMutableSection))
+            ) {
+                $dirty = true;
+            }
+
+            $configToWrite[$sectionName] = $changedSection;
+        }
+
+        if ($dirty) {
+            // sort config sections by how early they appear in the file chain
+            $self = $this;
+            uksort($configToWrite, function ($sectionNameLhs, $sectionNameRhs) use ($self) {
+                $lhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameLhs);
+                $rhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameRhs);
+
+                if ($lhsIndex == $rhsIndex) {
+                    return 0;
+                } else if ($lhsIndex < $rhsIndex) {
+                    return -1;
+                } else {
+                    return 1;
+                }
+            });
+
+            $writer = new IniWriter();
+            return $writer->writeToString($configToWrite, $header);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Reloads settings from disk.
+     */
+    public function reload($defaultSettingsFiles = array(), $userSettingsFile = null)
+    {
+        if (!empty($defaultSettingsFiles)
+            || !empty($userSettingsFile)
+        ) {
+            $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile);
+        }
+
+        $reader = new IniReader();
+        foreach ($this->settingsChain as $file => $ignore) {
+            if (is_readable($file)) {
+                try {
+                    $this->settingsChain[$file] = $reader->readFile($file);
+                } catch (IniReadingException $ex) {
+                    $message = Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($file, "parse_ini_file()"));
+                    throw new IniReadingException($message, $code = 0, $ex);
+                }
+            }
+        }
+
+        $this->mergedSettings = $this->mergeFileSettings();
+    }
+
+    private function resetSettingsChain($defaultSettingsFiles, $userSettingsFile)
+    {
+        $this->settingsChain = array();
+
+        if (!empty($defaultSettingsFiles)) {
+            foreach ($defaultSettingsFiles as $file) {
+                $this->settingsChain[$file] = null;
+            }
+        }
+
+        if (!empty($userSettingsFile)) {
+            $this->settingsChain[$userSettingsFile] = null;
+        }
+    }
+
+    protected function mergeFileSettings()
+    {
+        $mergedSettings = $this->getMergedDefaultSettings();
+
+        $userSettings = end($this->settingsChain) ?: array();
+        foreach ($userSettings as $sectionName => $section) {
+            if (!isset($mergedSettings[$sectionName])) {
+                $mergedSettings[$sectionName] = $section;
+            } else {
+                // the last user settings file completely overwrites INI sections. the other files in the chain
+                // can add to array options
+                $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section);
+            }
+        }
+
+        return $mergedSettings;
+    }
+
+    protected function getMergedDefaultSettings()
+    {
+        $userSettingsFile = $this->getUserSettingsFile();
+
+        $mergedSettings = array();
+        foreach ($this->settingsChain as $file => $settings) {
+            if ($file == $userSettingsFile
+                || empty($settings)
+            ) {
+                continue;
+            }
+
+            foreach ($settings as $sectionName => $section) {
+                if (!isset($mergedSettings[$sectionName])) {
+                    $mergedSettings[$sectionName] = $section;
+                } else {
+                    $mergedSettings[$sectionName] = $this->array_merge_recursive_distinct($mergedSettings[$sectionName], $section);
+                }
+            }
+        }
+        return $mergedSettings;
+    }
+
+    protected function getUserSettingsFile()
+    {
+        // the user settings file is the last key in $settingsChain
+        end($this->settingsChain);
+        return key($this->settingsChain);
+    }
+
+    /**
+     * Comparison function
+     *
+     * @param mixed $elem1
+     * @param mixed $elem2
+     * @return int;
+     */
+    public static function compareElements($elem1, $elem2)
+    {
+        if (is_array($elem1)) {
+            if (is_array($elem2)) {
+                return strcmp(serialize($elem1), serialize($elem2));
+            }
+
+            return 1;
+        }
+
+        if (is_array($elem2)) {
+            return -1;
+        }
+
+        if ((string)$elem1 === (string)$elem2) {
+            return 0;
+        }
+
+        return ((string)$elem1 > (string)$elem2) ? 1 : -1;
+    }
+
+    /**
+     * Compare arrays and return difference, such that:
+     *
+     *     $modified = array_merge($original, $difference);
+     *
+     * @param array $original original array
+     * @param array $modified modified array
+     * @return array differences between original and modified
+     */
+    public function arrayUnmerge($original, $modified)
+    {
+        // return key/value pairs for keys in $modified but not in $original
+        // return key/value pairs for keys in both $modified and $original, but values differ
+        // ignore keys that are in $original but not in $modified
+
+        return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements'));
+    }
+
+    /**
+     * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
+     * keys to arrays rather than overwriting the value in the first array with the duplicate
+     * value in the second array, as array_merge does. I.e., with array_merge_recursive,
+     * this happens (documented behavior):
+     *
+     * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
+     *     => array('key' => array('org value', 'new value'));
+     *
+     * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
+     * Matching keys' values in the second array overwrite those in the first array, as is the
+     * case with array_merge, i.e.:
+     *
+     * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
+     *     => array('key' => array('new value'));
+     *
+     * Parameters are passed by reference, though only for performance reasons. They're not
+     * altered by this function.
+     *
+     * @param array $array1
+     * @param array $array2
+     * @return array
+     * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
+     * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
+     */
+    private function array_merge_recursive_distinct ( array &$array1, array &$array2 )
+    {
+        $merged = $array1;
+        foreach ( $array2 as $key => &$value ) {
+            if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) {
+                $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value );
+            } else {
+                $merged [$key] = $value;
+            }
+        }
+        return $merged;
+    }
+
+    public function findIndexOfFirstFileWithSection($sectionName)
+    {
+        $count = 0;
+        foreach ($this->settingsChain as $file => $settings) {
+            if (isset($settings[$sectionName])) {
+                break;
+            }
+
+            ++$count;
+        }
+        return $count;
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/TrackerTest.php b/tests/PHPUnit/Integration/TrackerTest.php
index 9f261074eaaca41708590707701b94019dc22347..260e8d721b7e4698074c586914350c7e8ec6320d 100644
--- a/tests/PHPUnit/Integration/TrackerTest.php
+++ b/tests/PHPUnit/Integration/TrackerTest.php
@@ -148,7 +148,11 @@ class TrackerTest extends IntegrationTestCase
         $this->assertTrue(Config::getInstance()->existsLocalConfig());
 
         $this->removeConfigFile();
-        Config::getInstance()->clear();
+        try {
+            Config::getInstance()->reload();
+        } catch (\Exception $ex) {
+            // ignore config file not found exception
+        }
 
         $this->assertFalse(Config::getInstance()->existsLocalConfig());
 
diff --git a/tests/PHPUnit/TestingEnvironment.php b/tests/PHPUnit/TestingEnvironment.php
index 3acc202a911b9af8ea672c7569b97df618f387d1..388efa1c1f737869ef1d45660a469f256d9fbd9d 100644
--- a/tests/PHPUnit/TestingEnvironment.php
+++ b/tests/PHPUnit/TestingEnvironment.php
@@ -146,9 +146,11 @@ class Piwik_TestingEnvironment
             \Piwik\Profiler::setupProfilerXHProf($mainRun = false, $setupDuringTracking = true);
         }
 
-        Config::setSingletonInstance(new Config(
-            $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
-        ));
+        if ($testingEnvironment->dontUseTestConfig) {
+            Config::setSingletonInstance(new Config(
+                $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
+            ));
+        }
 
         // Apply DI config from the fixture
         if ($testingEnvironment->fixtureClass) {
@@ -172,11 +174,11 @@ class Piwik_TestingEnvironment
             }
         });
         if (!$testingEnvironment->dontUseTestConfig) {
-            Piwik::addAction('Config.createConfigSingleton', function(Config $config, &$cache, &$local) use ($testingEnvironment) {
+            Piwik::addAction('Config.createConfigSingleton', function(Config $config, &$cache) use ($testingEnvironment) {
                 $config->setTestEnvironment($testingEnvironment->configFileLocal, $testingEnvironment->configFileGlobal, $testingEnvironment->configFileCommon);
 
                 if ($testingEnvironment->configFileLocal) {
-                    $local['General']['session_save_handler'] = 'dbtable';
+                    $config->General['session_save_handler'] = 'dbtable';
                 }
 
                 $manager = \Piwik\Plugin\Manager::getInstance();
@@ -187,19 +189,19 @@ class Piwik_TestingEnvironment
 
                 sort($pluginsToLoad);
 
-                $local['Plugins'] = array('Plugins' => $pluginsToLoad);
+                $config->Plugins = array('Plugins' => $pluginsToLoad);
 
-                $local['log']['log_writers'] = array('file');
+                $config->log['log_writers'] = array('file');
 
                 $manager->unloadPlugins();
 
                 // TODO: replace this and below w/ configOverride use
                 if ($testingEnvironment->tablesPrefix) {
-                    $cache['database']['tables_prefix'] = $testingEnvironment->tablesPrefix;
+                    $config->database['tables_prefix'] = $testingEnvironment->tablesPrefix;
                 }
 
                 if ($testingEnvironment->dbName) {
-                    $cache['database']['dbname'] = $testingEnvironment->dbName;
+                    $config->database['dbname'] = $testingEnvironment->dbName;
                 }
 
                 if ($testingEnvironment->configOverride) {
diff --git a/tests/PHPUnit/Unit/Config/IniFileChainTest.php b/tests/PHPUnit/Unit/Config/IniFileChainTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b806febef68e7898b2b5bf1afb109fb0e918748f
--- /dev/null
+++ b/tests/PHPUnit/Unit/Config/IniFileChainTest.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Unit\Config;
+
+use PHPUnit_Framework_TestCase;
+use Piwik\Config\IniFileChain;
+
+/**
+ * @group Core
+ */
+class IniFileChainTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Data provider for testCompareElements
+     */
+    public function getCompareElementsData()
+    {
+        return array(
+            array('string = string', array(
+                'a', 'a', 0,
+            )),
+            array('string > string', array(
+                'b', 'a', 1,
+            )),
+            array('string < string', array(
+                'a', 'b', -1,
+            )),
+            array('string vs array', array(
+                'a', array('a'), -1,
+            )),
+            array('array vs string', array(
+                array('a'), 'a', 1,
+            )),
+            array('array = array', array(
+                array('a'), array('a'), 0,
+            )),
+            array('array > array', array(
+                array('b'), array('a'), 1,
+            )),
+            array('array < array', array(
+                array('a'), array('b'), -1,
+            )),
+        );
+    }
+
+    /**
+     * @dataProvider getCompareElementsData
+     */
+    public function test_compareElements_CorrectlyComparesElements($description, $test)
+    {
+        list($a, $b, $expected) = $test;
+
+        $result = IniFileChain::compareElements($a, $b);
+        $this->assertEquals($expected, $result, $description);
+    }
+
+    /**
+     * Dataprovider for testArrayUnmerge
+     * @return array
+     */
+    public function getArrayUnmergeData()
+    {
+        return array(
+            array('description of test', array(
+                array(),
+                array(),
+            )),
+            array('override with empty', array(
+                array('login' => 'root', 'password' => 'b33r'),
+                array('password' => ''),
+            )),
+            array('override with non-empty', array(
+                array('login' => 'root', 'password' => ''),
+                array('password' => 'b33r'),
+            )),
+            array('add element', array(
+                array('login' => 'root', 'password' => ''),
+                array('auth' => 'Login'),
+            )),
+            array('override with empty array', array(
+                array('headers' => ''),
+                array('headers' => array()),
+            )),
+            array('override with array', array(
+                array('headers' => ''),
+                array('headers' => array('Content-Length', 'Content-Type')),
+            )),
+            array('override an array', array(
+                array('headers' => array()),
+                array('headers' => array('Content-Length', 'Content-Type')),
+            )),
+            array('override similar arrays', array(
+                array('headers' => array('Content-Length', 'Set-Cookie')),
+                array('headers' => array('Content-Length', 'Content-Type')),
+            )),
+            array('override dyslexic arrays', array(
+                array('headers' => array('Content-Type', 'Content-Length')),
+                array('headers' => array('Content-Length', 'Content-Type')),
+            )),
+        );
+    }
+
+    /**
+     * @dataProvider getArrayUnmergeData
+     */
+    public function test_ArrayUnmerge_ReturnsCorrectDiff($description, $test)
+    {
+        $configWriter = new IniFileChain(array(), null);
+
+        list($a, $b) = $test;
+
+        $combined = array_merge($a, $b);
+
+        $diff = $configWriter->arrayUnmerge($a, $combined);
+
+        // expect $b == $diff
+        $this->assertEquals(serialize($b), serialize($diff), $description);
+    }
+
+    public function getMergingTestData()
+    {
+        return array(
+            array('test default settings are merged recursively',
+                array( // default settings
+                    __DIR__ . '/test_files/default_settings_1.ini.php',
+                    __DIR__ . '/test_files/empty.ini.php',
+                    __DIR__ . '/test_files/default_settings_2.ini.php'
+                ),
+                __DIR__ . '/tests_files/empty.ini.php', // user settings
+                array( // expected
+                    'Section1' => array(
+                        'var1' => 'overriddenValue1',
+                        'var3' => array(
+                            'overriddenValue2',
+                            'overriddenValue3'
+                        )
+                    ),
+                    'Section2' => array(
+                        'var4' => 'value5'
+                    )
+                )
+            ),
+
+            array('test user settings completely overwrite default',
+                array( // default settings
+                    __DIR__ . '/test_files/default_settings_1.ini.php'
+                ),
+                __DIR__ . '/test_files/default_settings_2.ini.php', // user settings
+                array( // expected
+                    'Section1' => array(
+                        'var1' => 'overriddenValue1',
+                        'var3' => array(
+                            'overriddenValue2',
+                            'overriddenValue3'
+                        )
+                    ),
+                    'Section2' => array(
+                        'var4' => 'value5'
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * @dataProvider getMergingTestData
+     */
+    public function test_construct_MergesFileData_Correctly($testDescription, $defaultSettingFiles, $userSettingsFile, $expected)
+    {
+        $fileChain = new IniFileChain($defaultSettingFiles, $userSettingsFile);
+        $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+    }
+
+    /**
+     * @dataProvider getMergingTestData
+     */
+    public function test_reload_MergesFileData_Correctly($testDescription, $defaultSettingsFiles, $userSettingsFile, $expected)
+    {
+        $fileChain = new IniFileChain();
+        $fileChain->reload($defaultSettingsFiles, $userSettingsFile);
+        $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed");
+    }
+
+    public function test_get_ReturnsReferenceToSettingsSection()
+    {
+        $fileChain = new IniFileChain(
+            array(__DIR__ . '/test_files/default_settings_1.ini.php')
+        );
+
+        $data =& $fileChain->get('Section1');
+
+        $this->assertEquals(array('var1' => 'value2', 'var3' => array('value3', 'value4')), $data);
+
+        $data['var1'] = 'changed';
+        $data['var3'][] = 'newValue';
+
+        $this->assertEquals(array('var1' => 'changed', 'var3' => array('value3', 'value4', 'newValue')), $fileChain->get('Section1'));
+    }
+
+    public function test_get_ReturnsReferenceToSettingsSection_EvenIfSettingsIsEmpty()
+    {
+        $fileChain = new IniFileChain(array(__DIR__ . '/test_files/empty.ini.php'));
+
+        $data =& $fileChain->get('Section');
+        $this->assertEquals(array(), $data);
+
+        $data['var1'] = 'changed';
+        $this->assertEquals(array('var1' => 'changed'), $fileChain->get('Section'));
+    }
+
+    public function test_getAll_ReturnsReferenceToAllSettings()
+    {
+        $fileChain = new IniFileChain();
+
+        $data =& $fileChain->getAll();
+        $data['var'] = 'value';
+
+        $this->assertEquals(array('var' => 'value'), $fileChain->getAll());
+    }
+
+    public function test_set_CorrectlySetsSettingValue()
+    {
+        $fileChain = new IniFileChain();
+
+        $fileChain->set('var', 'value');
+
+        $this->assertEquals(array('var' => 'value'), $fileChain->getAll());
+    }
+
+    public function test_getFrom_CorrectlyGetsSettingsFromFile_AndNotCurrentModifiedSettings()
+    {
+        $defaultSettingsPath = __DIR__ . '/test_files/default_settings_1.ini.php';
+
+        $fileChain = new IniFileChain(
+            array($defaultSettingsPath),
+            __DIR__ . '/test_files/default_settings_2.ini.php'
+        );
+
+        $this->assertEquals(array('var1' => 'value2', 'var3' => array('value3', 'value4')), $fileChain->getFrom($defaultSettingsPath, 'Section1'));
+    }
+
+    public function getTestDataForDumpTest()
+    {
+        return array(
+            array(
+                array( // default settings
+                    __DIR__ . '/test_files/default_settings_1.ini.php'
+                ),
+                __DIR__ . '/test_files/default_settings_2.ini.php', // user settings
+                "; some header\n",
+                "; some header\n[Section1]\nvar1 = \"overriddenValue1\"\nvar3[] = \"overriddenValue2\"\nvar3[] = \"overriddenValue3\"\n\n[Section2]\nvar4 = \"value5\"\n\n",
+                "; some header\n[Section1]\nvar1 = \"overriddenValue1\"\nvar3[] = \"overriddenValue2\"\nvar3[] = \"overriddenValue3\"\n\n"
+            )
+        );
+    }
+
+    /**
+     * @dataProvider getTestDataForDumpTest
+     */
+    public function test_dump_CorrectlyGeneratesIniString_ForAllCurrentSettings(
+        $defaultSettingsFiles, $userSettingsFile, $header, $expectedDump)
+    {
+        $fileChain = new IniFileChain($defaultSettingsFiles, $userSettingsFile);
+
+        $actualOutput = $fileChain->dump($header);
+        $this->assertEquals($expectedDump, $actualOutput);
+    }
+
+    /**
+     * @dataProvider getTestDataForDumpTest
+     */
+    public function test_dumpChanges_CorrectlyGeneratesMinimalUserSettingsIniString(
+        $defaultSettingsFiles, $userSettingsFile, $header, $expectedDump, $expectedDumpChanges)
+    {
+        $fileChain = new IniFileChain($defaultSettingsFiles, $userSettingsFile);
+
+        $actualOutput = $fileChain->dumpChanges($header);
+        $this->assertEquals($expectedDumpChanges, $actualOutput);
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php b/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php
new file mode 100644
index 0000000000000000000000000000000000000000..2eb4fdd926f77beb99d44faef8aa0203ff03f799
--- /dev/null
+++ b/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php
@@ -0,0 +1,9 @@
+[Section1]
+
+var1 = "value2"
+var3[] = "value3"
+var3[] = "value4"
+
+[Section2]
+
+var4 = "value5"
\ No newline at end of file
diff --git a/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php b/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php
new file mode 100644
index 0000000000000000000000000000000000000000..48e9e0f644652f83158127646e741265954cf1bf
--- /dev/null
+++ b/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php
@@ -0,0 +1,6 @@
+[Section1]
+
+var3[] = "overriddenValue2"
+var3[] = "overriddenValue3"
+
+var1 = "overriddenValue1"
diff --git a/tests/PHPUnit/Unit/Config/test_files/empty.ini.php b/tests/PHPUnit/Unit/Config/test_files/empty.ini.php
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/PHPUnit/Unit/ConfigTest.php b/tests/PHPUnit/Unit/ConfigTest.php
index 2fe32fec68cbb64a3354de73978b6aa78ad91575..d161743e34ce99e12a12d22962c0acd5309b5f5f 100644
--- a/tests/PHPUnit/Unit/ConfigTest.php
+++ b/tests/PHPUnit/Unit/ConfigTest.php
@@ -11,6 +11,34 @@ namespace Piwik\Tests\Unit;
 use PHPUnit_Framework_TestCase;
 use Piwik\Config;
 
+class DumpConfigTestMockIniFileChain extends Config\IniFileChain
+{
+    public function __construct($settingsChain, $mergedSettings)
+    {
+        parent::__construct();
+
+        $this->settingsChain = $settingsChain;
+        $this->mergedSettings = $mergedSettings;
+    }
+}
+
+class DumpConfigTestMockConfig extends Config
+{
+    public function __construct($configLocal, $configGlobal, $configCommon, $configCache)
+    {
+        parent::__construct();
+
+        $this->settings = new DumpConfigTestMockIniFileChain(
+            array(
+                $this->pathGlobal => $configGlobal,
+                $this->pathCommon => $configCommon,
+                $this->pathLocal => $configLocal,
+            ),
+            $configCache
+        );
+    }
+}
+
 /**
  * @group Core
  */
@@ -115,113 +143,6 @@ class ConfigTest extends PHPUnit_Framework_TestCase
         Config::getInstance()->clear();
     }
 
-    /**
-     * Dateprovider for testCompareElements
-     */
-    public function getCompareElementsData()
-    {
-        return array(
-            array('string = string', array(
-                'a', 'a', 0,
-            )),
-            array('string > string', array(
-                'b', 'a', 1,
-            )),
-            array('string < string', array(
-                'a', 'b', -1,
-            )),
-            array('string vs array', array(
-                'a', array('a'), -1,
-            )),
-            array('array vs string', array(
-                array('a'), 'a', 1,
-            )),
-            array('array = array', array(
-                array('a'), array('a'), 0,
-            )),
-            array('array > array', array(
-                array('b'), array('a'), 1,
-            )),
-            array('array < array', array(
-                array('a'), array('b'), -1,
-            )),
-        );
-    }
-
-    /**
-     * @dataProvider getCompareElementsData
-     */
-    public function testCompareElements($description, $test)
-    {
-        list($a, $b, $expected) = $test;
-
-        $result = Config::compareElements($a, $b);
-        $this->assertEquals($expected, $result, $description);
-    }
-
-    /**
-     * Dataprovider for testArrayUnmerge
-     * @return array
-     */
-    public function getArrayUnmergeData()
-    {
-        return array(
-            array('description of test', array(
-                array(),
-                array(),
-            )),
-            array('override with empty', array(
-                array('login' => 'root', 'password' => 'b33r'),
-                array('password' => ''),
-            )),
-            array('override with non-empty', array(
-                array('login' => 'root', 'password' => ''),
-                array('password' => 'b33r'),
-            )),
-            array('add element', array(
-                array('login' => 'root', 'password' => ''),
-                array('auth' => 'Login'),
-            )),
-            array('override with empty array', array(
-                array('headers' => ''),
-                array('headers' => array()),
-            )),
-            array('override with array', array(
-                array('headers' => ''),
-                array('headers' => array('Content-Length', 'Content-Type')),
-            )),
-            array('override an array', array(
-                array('headers' => array()),
-                array('headers' => array('Content-Length', 'Content-Type')),
-            )),
-            array('override similar arrays', array(
-                array('headers' => array('Content-Length', 'Set-Cookie')),
-                array('headers' => array('Content-Length', 'Content-Type')),
-            )),
-            array('override dyslexic arrays', array(
-                array('headers' => array('Content-Type', 'Content-Length')),
-                array('headers' => array('Content-Length', 'Content-Type')),
-            )),
-        );
-    }
-
-    /**
-     * @dataProvider getArrayUnmergeData
-     */
-    public function testArrayUnmerge($description, $test)
-    {
-        $configWriter = Config::getInstance();
-
-        list($a, $b) = $test;
-
-        $combined = array_merge($a, $b);
-
-        $diff = $configWriter->array_unmerge($a, $combined);
-
-        // expect $b == $diff
-        $this->assertEquals(serialize($b), serialize($diff), $description);
-    }
-
     /**
      * Dataprovider for testDumpConfig
      */
@@ -337,7 +258,7 @@ class ConfigTest extends PHPUnit_Framework_TestCase
                 array('Tracker' => array('anonymize' => 1)),  // local
                 array('General' => array('debug' => 1)),      // global
                 array(),                                      // common
-                array('General' => array('debug' => 2)),
+                array('General' => array('debug' => 2), 'Tracker' => array('anonymize' => 1)),
                 $header . "[General]\ndebug = 2\n\n[Tracker]\nanonymize = 1\n\n",
             )),
 
@@ -347,7 +268,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase
                 array('General' => array('debug' => 0),       // global
                       'Tracker' => array('anonymize' => 0)),
                 array(),                                      // common
-                array('Tracker' => array('anonymize' => 2)),
+                array('Tracker' => array('anonymize' => 2),
+                      'General' => array('debug' => 1)),
                 $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 2\n\n",
             )),
 
@@ -357,7 +279,9 @@ class ConfigTest extends PHPUnit_Framework_TestCase
                 array('General' => array('debug' => 0),       // global
                       'Tracker' => array('anonymize' => 0)),
                 array(),                                      // common
-                array('Segment' => array('dimension' => 'foo')),
+                array('Segment' => array('dimension' => 'foo'),
+                      'Tracker' => array('anonymize' => 1),   // local
+                      'General' => array('debug' => 1)),
                 $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 1\n\n[Segment]\ndimension = \"foo\"\n\n",
             )),
 
@@ -406,7 +330,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase
                 array('CommonCategory' => array('settingCommon' => 'common',            // common
                                                 'settingCommon2' => 'common2')),
                 array('CommonCategory' => array('settingCommon2' => 'common2',
-                                                'newSetting' => 'newValue')),
+                                                'newSetting' => 'newValue'),
+                      'General' => array('key' => 'value')),
                 $header . "[General]\nkey = \"value\"\n\n[CommonCategory]\nnewSetting = \"newValue\"\n\n",
             )),
 
@@ -417,7 +342,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase
                 array('CommonCategory' => array('settingCommon' => 'common',            // common
                     'settingCommon2' => 'common2')),
                 array('CommonCategory' => array('settingCommon2' => 'common2',
-                    'newSetting' => 'newValue')),
+                                                'newSetting' => 'newValue'),
+                    'General' => array('key' => '$value', 'key2' => '${value}')),
                 $header . "[General]\nkey = \"&#36;value\"\nkey2 = \"&#36;{value}\"\n\n[CommonCategory]\nnewSetting = \"newValue\"\n\n",
             )),
         );
@@ -429,11 +355,11 @@ class ConfigTest extends PHPUnit_Framework_TestCase
      */
     public function testDumpConfig($description, $test)
     {
-        $config = Config::getInstance();
-
         list($configLocal, $configGlobal, $configCommon, $configCache, $expected) = $test;
 
-        $output = $config->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache);
+        $config = new DumpConfigTestMockConfig($configLocal, $configGlobal, $configCommon, $configCache);
+
+        $output = $config->dumpConfig();
         $this->assertEquals($expected, $output, $description);
     }
 
@@ -443,11 +369,26 @@ class ConfigTest extends PHPUnit_Framework_TestCase
         $globalFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/global.ini.php';
         $commonFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/common.config.ini.php';
 
-        $config = Config::getInstance();
-        $config->setTestEnvironment($userFile, $globalFile, $commonFile);
-        $config->init();
+        $config = new Config($globalFile, $userFile, $commonFile);
 
         $this->assertEquals('${@piwik(crash))}', $config->Category['key3']);
     }
+
+    public function test_forceSave_writesNothingIfThereAreNoChanges()
+    {
+        $sourceConfigFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/config.ini.php';
+        $configFile = PIWIK_INCLUDE_PATH . '/tmp/tmp.config.ini.php';
+
+        @unlink($configFile);
+        copy($sourceConfigFile, $configFile);
+
+        $config = new Config($sourceConfigFile, $configFile);
+        $config->reload();
+        $config->forceSave();
+
+        $this->assertEquals(file_get_contents($sourceConfigFile), file_get_contents($configFile));
+
+        @unlink($configFile);
+    }
 }