From 67829227ce2d0c4473466acbc42118a7a150b14c Mon Sep 17 00:00:00 2001
From: diosmosis <benaka@piwik.pro>
Date: Wed, 8 Apr 2015 20:36:22 -0700
Subject: [PATCH] Added environment validation system test (mostly failing)
 that tests Piwik's behavior when INI files are gone or corrupt from each
 Piwik endpoint (tracker/reporting UI/console). Hacked test code to make it
 possible and for some tests to pass.

---
 config/global.php                             |  10 +-
 core/Application/Environment.php              |   2 +
 .../Kernel/EnvironmentValidator.php           |  14 +-
 .../IniSettingsProvider.php                   |  48 +++-
 core/Application/Kernel/PluginList.php        |   7 +
 .../Kernel/PluginList/IniPluginList.php       |  16 +-
 core/Config.php                               | 105 +++------
 core/Config/IniFileChain.php                  |   7 +-
 core/ExceptionHandler.php                     |   6 +-
 core/Plugin/Manager.php                       |   5 +-
 core/Translation/Translator.php               |   3 +-
 piwik.php                                     |   4 +
 tests/PHPUnit/Framework/Fixture.php           |   5 +-
 tests/PHPUnit/Framework/Mock/TestConfig.php   |   8 +-
 .../System/EnvironmentValidationTest.php      | 222 ++++++++++++++++++
 tests/PHPUnit/TestingEnvironment.php          |  65 +++--
 tests/PHPUnit/proxy/console                   |   7 +
 17 files changed, 414 insertions(+), 120 deletions(-)
 create mode 100644 tests/PHPUnit/System/EnvironmentValidationTest.php
 create mode 100644 tests/PHPUnit/proxy/console

diff --git a/config/global.php b/config/global.php
index aff73fcdba..0cb88f5b3d 100644
--- a/config/global.php
+++ b/config/global.php
@@ -1,6 +1,7 @@
 <?php
 
 use Interop\Container\ContainerInterface;
+use Interop\Container\Exception\NotFoundException;
 use Piwik\Cache\Eager;
 use Piwik\SettingsServer;
 
@@ -44,7 +45,13 @@ return array(
         return $cache;
     },
     'Piwik\Cache\Backend' => function (ContainerInterface $c) {
-        return \Piwik\Cache::buildBackend($c->get('ini.Cache.backend'));
+        try {
+            $backend = $c->get('ini.Cache.backend');
+        } catch (NotFoundException $ex) {
+            $backend = 'chained'; // happens if global.ini.php is not available
+        }
+
+        return \Piwik\Cache::buildBackend($backend);
     },
     'cache.eager.cache_id' => function () {
         return 'eagercache-' . str_replace(array('.', '-'), '', \Piwik\Version::VERSION) . '-';
@@ -54,5 +61,4 @@ return array(
 
     'Piwik\Translation\Loader\LoaderInterface' => DI\object('Piwik\Translation\Loader\LoaderCache')
         ->constructor(DI\link('Piwik\Translation\Loader\JsonFileLoader')),
-
 );
diff --git a/core/Application/Environment.php b/core/Application/Environment.php
index 38a6c2eb5c..6687b015c4 100644
--- a/core/Application/Environment.php
+++ b/core/Application/Environment.php
@@ -150,6 +150,8 @@ class Environment
      */
     protected function getGlobalSettings()
     {
+        // TODO: need to be able to set path global/local/etc. which is in DI... for now works because TestingEnvironment creates
+        //       singleton instance before this method.
         return IniSettingsProvider::getSingletonInstance();
     }
 
diff --git a/core/Application/Kernel/EnvironmentValidator.php b/core/Application/Kernel/EnvironmentValidator.php
index 9474652ac9..9e116a7cb1 100644
--- a/core/Application/Kernel/EnvironmentValidator.php
+++ b/core/Application/Kernel/EnvironmentValidator.php
@@ -12,6 +12,7 @@ use Piwik\Application\Kernel\GlobalSettingsProvider\IniSettingsProvider;
 use Piwik\Common;
 use Piwik\Piwik;
 use Piwik\SettingsServer;
+use Piwik\Translate;
 use Piwik\Translation\Translator;
 
 /**
@@ -33,6 +34,7 @@ class EnvironmentValidator
     public function __construct(GlobalSettingsProvider $settingsProvider, Translator $translator)
     {
         $this->iniSettingsProvider = $settingsProvider;
+        $this->translator = $translator;
     }
 
     public function validate()
@@ -40,23 +42,23 @@ class EnvironmentValidator
         $inTrackerRequest = SettingsServer::isTrackerApiRequest();
         $inConsole = Common::isPhpCliMode();
 
-        $this->checkConfigFileExists('global.ini.php');
-        $this->checkConfigFileExists('config.ini.php', $startInstaller = !$inTrackerRequest && !$inConsole);
+        $this->checkConfigFileExists($this->iniSettingsProvider->getPathGlobal());
+        $this->checkConfigFileExists($this->iniSettingsProvider->getPathLocal(), $startInstaller = !$inTrackerRequest && !$inConsole);
     }
 
     /**
-     * @param $filename
+     * @param $path
      * @param bool $startInstaller
      * @throws \Exception
      */
-    private function checkConfigFileExists($filename, $startInstaller = false)
+    private function checkConfigFileExists($path, $startInstaller = false)
     {
-        $path = PIWIK_INCLUDE_PATH . '/config/' . $filename;
-
         if (is_readable($path)) {
             return;
         }
 
+        Translate::loadAllTranslations();
+
         $message = $this->translator->translate('General_ExceptionConfigurationFileNotFound', array($path));
         $exception = new \Exception($message);
 
diff --git a/core/Application/Kernel/GlobalSettingsProvider/IniSettingsProvider.php b/core/Application/Kernel/GlobalSettingsProvider/IniSettingsProvider.php
index 07dd25606e..75123b1645 100644
--- a/core/Application/Kernel/GlobalSettingsProvider/IniSettingsProvider.php
+++ b/core/Application/Kernel/GlobalSettingsProvider/IniSettingsProvider.php
@@ -27,6 +27,21 @@ class IniSettingsProvider implements GlobalSettingsProvider
      */
     private $iniFileChain;
 
+    /**
+     * @var string
+     */
+    protected $pathGlobal = null;
+
+    /**
+     * @var string
+     */
+    protected $pathCommon = null;
+
+    /**
+     * @var string
+     */
+    protected $pathLocal = null;
+
     /**
      * @param string|null $pathGlobal Path to the global.ini.php file. Or null to use the default.
      * @param string|null $pathLocal Path to the config.ini.php file. Or null to use the default.
@@ -34,11 +49,21 @@ class IniSettingsProvider implements GlobalSettingsProvider
      */
     public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
     {
-        $pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
-        $pathCommon = $pathCommon ?: Config::getCommonConfigPath();
-        $pathLocal = $pathLocal ?: Config::getLocalConfigPath();
+        $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
+        $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
+        $this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
+
+        $this->iniFileChain = new IniFileChain();
+        $this->reload();
+    }
+
+    public function reload($pathGlobal = null, $pathLocal = null, $pathCommon = null)
+    {
+        $this->pathGlobal = $pathGlobal ?: $this->pathGlobal;
+        $this->pathCommon = $pathCommon ?: $this->pathCommon;
+        $this->pathLocal = $pathLocal ?: $this->pathLocal;
 
-        $this->iniFileChain = new IniFileChain(array($pathGlobal, $pathCommon), $pathLocal);
+        $this->iniFileChain->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal);
     }
 
     public function &getSection($name)
@@ -57,6 +82,21 @@ class IniSettingsProvider implements GlobalSettingsProvider
         return $this->iniFileChain;
     }
 
+    public function getPathGlobal()
+    {
+        return $this->pathGlobal;
+    }
+
+    public function getPathLocal()
+    {
+        return $this->pathLocal;
+    }
+
+    public function getPathCommon()
+    {
+        return $this->pathCommon;
+    }
+
     public static function getSingletonInstance($pathGlobal = null, $pathLocal = null, $pathCommon = null)
     {
         if (self::$instance === null) {
diff --git a/core/Application/Kernel/PluginList.php b/core/Application/Kernel/PluginList.php
index 9d5e03cc0a..672110853b 100644
--- a/core/Application/Kernel/PluginList.php
+++ b/core/Application/Kernel/PluginList.php
@@ -23,4 +23,11 @@ interface PluginList
      * @return string[]
      */
     public function getActivatedPlugins();
+
+    /**
+     * Returns the list of plugins that are bundled with Piwik.
+     *
+     * @return string[]
+     */
+    public function getPluginsBundledWithPiwik();
 }
\ No newline at end of file
diff --git a/core/Application/Kernel/PluginList/IniPluginList.php b/core/Application/Kernel/PluginList/IniPluginList.php
index 6636cc4fd6..8825661e82 100644
--- a/core/Application/Kernel/PluginList/IniPluginList.php
+++ b/core/Application/Kernel/PluginList/IniPluginList.php
@@ -9,16 +9,19 @@
 namespace Piwik\Application\Kernel\PluginList;
 
 use Piwik\Application\Kernel\GlobalSettingsProvider;
+use Piwik\Application\Kernel\GlobalSettingsProvider\IniSettingsProvider;
 use Piwik\Application\Kernel\PluginList;
 
 /**
  * Default implementation of the PluginList interface. Uses the [Plugins] section
  * in Piwik's INI config to get the activated plugins.
+ *
+ * Depends on IniSettingsProvider being used.
  */
 class IniPluginList implements PluginList
 {
     /**
-     * @var GlobalSettingsProvider
+     * @var IniSettingsProvider
      */
     private $settings;
 
@@ -35,4 +38,15 @@ class IniPluginList implements PluginList
         $section = $this->settings->getSection('Plugins');
         return @$section['Plugins'] ?: array();
     }
+
+    /**
+     * @return string[]
+     */
+    public function getPluginsBundledWithPiwik()
+    {
+        $pathGlobal = $this->settings->getPathGlobal();
+
+        $section = $this->settings->getIniFileChain()->getFrom($pathGlobal, 'Plugins');
+        return $section['Plugins'];
+    }
 }
\ No newline at end of file
diff --git a/core/Config.php b/core/Config.php
index 42cec66702..edc54d6a07 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -49,20 +49,13 @@ class Config extends Singleton
     const DEFAULT_COMMON_CONFIG_PATH = '/config/common.config.ini.php';
     const DEFAULT_GLOBAL_CONFIG_PATH = '/config/global.ini.php';
 
-    /**
-     * @var boolean
-     */
-    protected $pathGlobal = null;
-    protected $pathCommon = null;
-    protected $pathLocal = null;
-
     /**
      * @var boolean
      */
     protected $doNotWriteConfigInTests = false;
 
     /**
-     * @var IniFileChain
+     * @var IniSettingsProvider
      */
     protected $settings;
 
@@ -70,11 +63,7 @@ class Config extends Singleton
 
     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->settings = IniSettingsProvider::getSingletonInstance($pathGlobal, $pathLocal, $pathCommon)->getIniFileChain();
+        $this->settings = IniSettingsProvider::getSingletonInstance($pathGlobal, $pathLocal, $pathCommon);
     }
 
     /**
@@ -84,7 +73,7 @@ class Config extends Singleton
      */
     public function getLocalPath()
     {
-        return $this->pathLocal;
+        return $this->settings->getPathLocal();
     }
 
     /**
@@ -94,7 +83,7 @@ class Config extends Singleton
      */
     public function getGlobalPath()
     {
-        return $this->pathGlobal;
+        return $this->settings->getPathGlobal();
     }
 
     /**
@@ -104,7 +93,7 @@ class Config extends Singleton
      */
     public function getCommonPath()
     {
-        return $this->pathCommon;
+        return $this->settings->getPathCommon();
     }
 
     /**
@@ -121,37 +110,35 @@ class Config extends Singleton
             $this->doNotWriteConfigInTests = true;
         }
 
-        $this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
-        $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
-        $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
+        $this->reload($pathLocal, $pathGlobal, $pathCommon);
 
-        $this->reload();
+        $chain = $this->settings->getIniFileChain();
 
-        $databaseTestsSettings = $this->settings->get('database_tests'); // has to be __get otherwise when called from TestConfig, PHP will issue a NOTICE
+        $databaseTestsSettings = $chain->get('database_tests'); // has to be __get otherwise when called from TestConfig, PHP will issue a NOTICE
         if (!empty($databaseTestsSettings)) {
-            $this->settings->set('database', $databaseTestsSettings);
+            $chain->set('database', $databaseTestsSettings);
         }
 
         // Ensure local mods do not affect tests
         if (empty($pathGlobal)) {
-            $this->settings->set('Debug', $this->settings->getFrom($this->pathGlobal, 'Debug'));
-            $this->settings->set('mail', $this->settings->getFrom($this->pathGlobal, 'mail'));
-            $this->settings->set('General', $this->settings->getFrom($this->pathGlobal, 'General'));
-            $this->settings->set('Segments', $this->settings->getFrom($this->pathGlobal, 'Segments'));
-            $this->settings->set('Tracker', $this->settings->getFrom($this->pathGlobal, 'Tracker'));
-            $this->settings->set('Deletelogs', $this->settings->getFrom($this->pathGlobal, 'Deletelogs'));
-            $this->settings->set('Deletereports', $this->settings->getFrom($this->pathGlobal, 'Deletereports'));
-            $this->settings->set('Development', $this->settings->getFrom($this->pathGlobal, 'Development'));
+            $chain->set('Debug', $chain->getFrom($this->getGlobalPath(), 'Debug'));
+            $chain->set('mail', $chain->getFrom($this->getGlobalPath(), 'mail'));
+            $chain->set('General', $chain->getFrom($this->getGlobalPath(), 'General'));
+            $chain->set('Segments', $chain->getFrom($this->getGlobalPath(), 'Segments'));
+            $chain->set('Tracker', $chain->getFrom($this->getGlobalPath(), 'Tracker'));
+            $chain->set('Deletelogs', $chain->getFrom($this->getGlobalPath(), 'Deletelogs'));
+            $chain->set('Deletereports', $chain->getFrom($this->getGlobalPath(), 'Deletereports'));
+            $chain->set('Development', $chain->getFrom($this->getGlobalPath(), '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->settings->set('PluginsInstalled', array('PluginsInstalled' => array()));
+        $chain->set('PluginsInstalled', array('PluginsInstalled' => array()));
     }
 
     protected function postConfigTestEvent()
     {
-        Piwik::postTestEvent('Config.createConfigSingleton', array($this->settings, $this)  );
+        Piwik::postTestEvent('Config.createConfigSingleton', array($this->settings->getIniFileChain(), $this)  );
     }
 
     /**
@@ -230,7 +217,7 @@ class Config extends Singleton
         return $limits;
     }
 
-    protected static function getByDomainConfigPath()
+    public static function getByDomainConfigPath()
     {
         $host       = self::getHostname();
         $hostConfig = self::getLocalConfigInfoForHostname($host);
@@ -281,16 +268,15 @@ class Config extends Singleton
             throw new Exception('Piwik domain is not a valid looking hostname (' . $filename . ').');
         }
 
-        $this->pathLocal   = $hostConfig['path'];
-        $this->initialized = false;
+        $pathLocal = $hostConfig['path'];
 
         try {
-            $this->reload();
+            $this->reload($pathLocal);
         } catch (Exception $ex) {
             // pass (not required for local file to exist at this point)
         }
 
-        return $this->pathLocal;
+        return $pathLocal;
     }
 
     /**
@@ -300,7 +286,7 @@ class Config extends Singleton
      */
     public function isFileWritable()
     {
-        return is_writable($this->pathLocal);
+        return is_writable($this->settings->getPathLocal());
     }
 
     /**
@@ -330,11 +316,10 @@ class Config extends Singleton
      * @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.
      */
-    protected function reload()
+    protected function reload($pathLocal = null, $pathGlobal = null, $pathCommon = null)
     {
         $this->initialized = false;
-
-        $this->settings->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal);
+        $this->settings->reload($pathGlobal, $pathLocal, $pathCommon);
     }
 
     /**
@@ -342,7 +327,7 @@ class Config extends Singleton
      */
     public function existsLocalConfig()
     {
-        return is_readable($this->pathLocal);
+        return is_readable($this->getLocalPath());
     }
 
     public function deleteLocalConfig()
@@ -351,13 +336,6 @@ class Config extends Singleton
         unlink($configLocal);
     }
 
-/*    public function checkLocalConfigFound()
-    {
-        if (!$this->existsLocalConfig()) {
-            throw new ConfigNotFoundException(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathLocal)));
-        }
-    }*/
-
     /**
      * Decode HTML entities
      *
@@ -412,28 +390,21 @@ class Config extends Singleton
         if (!$this->initialized) {
             $this->initialized = true;
 
-            // this is done lazily and not in __construct so Installation will properly be triggered. ideally, it should be
-            // done in __construct, but the exception that is thrown depends on Translator which depends on Config. the
-            // circular dependency causes problems.
-            /*if (!SettingsServer::isTrackerApiRequest()) {
-                $this->checkLocalConfigFound();
-            }*/
-
             $this->postConfigTestEvent();
         }
 
-        $section =& $this->settings->get($name);
+        $section =& $this->settings->getIniFileChain()->get($name);
         return $section;
     }
 
     public function getFromGlobalConfig($name)
     {
-        return $this->settings->getFrom($this->pathGlobal, $name);
+        return $this->settings->getIniFileChain()->getFrom($this->getGlobalPath(), $name);
     }
 
     public function getFromCommonConfig($name)
     {
-        return $this->settings->getFrom($this->pathCommon, $name);
+        return $this->settings->getIniFileChain()->getFrom($this->getCommonPath(), $name);
     }
 
     /**
@@ -445,7 +416,7 @@ class Config extends Singleton
      */
     public function __set($name, $value)
     {
-        $this->settings->set($name, $value);
+        $this->settings->getIniFileChain()->set($name, $value);
     }
 
     /**
@@ -456,16 +427,18 @@ class Config extends Singleton
      */
     public function dumpConfig()
     {
-        $this->encodeValues($this->settings->getAll());
+        $chain = $this->settings->getIniFileChain();
+
+        $this->encodeValues($chain->getAll());
 
         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);
+            $dumpedString = $chain->dumpChanges($header);
 
-            $this->decodeValues($this->settings->getAll());
+            $this->decodeValues($chain->getAll());
         } catch (Exception $ex) {
-            $this->decodeValues($this->settings->getAll());
+            $this->decodeValues($chain->getAll());
 
             throw $ex;
         }
@@ -495,7 +468,7 @@ class Config extends Singleton
         if ($output !== null
             && $output !== false
         ) {
-            $success = @file_put_contents($this->pathLocal, $output);
+            $success = @file_put_contents($this->getLocalPath(), $output);
             if ($success === false) {
                 throw $this->getConfigNotWritableException();
             }
@@ -522,7 +495,7 @@ class Config extends Singleton
      */
     public function getConfigNotWritableException()
     {
-        $path = "config/" . basename($this->pathLocal);
+        $path = "config/" . basename($this->getLocalPath());
         return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", "")));
     }
 }
\ No newline at end of file
diff --git a/core/Config/IniFileChain.php b/core/Config/IniFileChain.php
index bffca9cbc6..95516921b8 100644
--- a/core/Config/IniFileChain.php
+++ b/core/Config/IniFileChain.php
@@ -206,12 +206,7 @@ class IniFileChain
         $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->settingsChain[$file] = $reader->readFile($file);
             }
         }
 
diff --git a/core/ExceptionHandler.php b/core/ExceptionHandler.php
index e68a058e4a..5b02cd77f6 100644
--- a/core/ExceptionHandler.php
+++ b/core/ExceptionHandler.php
@@ -78,7 +78,11 @@ class ExceptionHandler
             $logoHeaderUrl = $logo->getHeaderLogoUrl();
             $logoFaviconUrl = $logo->getPathUserFavicon();
         } catch (Exception $ex) {
-            Log::debug($ex);
+            try {
+                Log::debug($ex);
+            } catch (\Exception $otherEx) {
+                // DI container may not be setup at this point
+            }
         }
 
         $result = Piwik_GetErrorMessagePage($message, $debugTrace, true, true, $logoHeaderUrl, $logoFaviconUrl);
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 0d69083af7..1cdf9ebb2e 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -1280,9 +1280,7 @@ class Manager
      */
     protected function getPluginsFromGlobalIniConfigFile() // TODO: if this is only used for sorting, move to PluginList
     {
-        $pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins');
-        $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
-        return $pluginsBundledWithPiwik;
+        return $this->pluginList->getPluginsBundledWithPiwik();
     }
 
     /**
@@ -1292,7 +1290,6 @@ class Manager
     protected function isPluginEnabledByDefault($name)
     {
         $pluginsBundledWithPiwik = $this->getPluginsFromGlobalIniConfigFile();
-
         if(empty($pluginsBundledWithPiwik)) {
             return false;
         }
diff --git a/core/Translation/Translator.php b/core/Translation/Translator.php
index dda946ccd9..46503a0185 100644
--- a/core/Translation/Translator.php
+++ b/core/Translation/Translator.php
@@ -114,7 +114,8 @@ class Translator
      */
     public function getDefaultLanguage()
     {
-        return Config::getInstance()->General['default_language'];
+        $generalSection = Config::getInstance()->General;
+        return @$generalSection['default_language'] ?: 'en';
     }
 
     /**
diff --git a/piwik.php b/piwik.php
index 8eb43b39b4..b0915e38a1 100644
--- a/piwik.php
+++ b/piwik.php
@@ -8,6 +8,7 @@
  * @package Piwik
  */
 
+use Piwik\SettingsServer;
 use Piwik\Tracker\RequestSet;
 use Piwik\Tracker;
 use Piwik\Tracker\Handler;
@@ -48,6 +49,9 @@ require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Cache.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Request.php';
 require_once PIWIK_INCLUDE_PATH . '/core/Cookie.php';
 
+// TODO should move to Tracker application class later. currently needed for environment validation.
+SettingsServer::setIsTrackerApiRequest();
+
 $environment = new \Piwik\Application\Environment(null);
 $environment->init();
 
diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php
index fb8fd9e54d..87ccf6d071 100644
--- a/tests/PHPUnit/Framework/Fixture.php
+++ b/tests/PHPUnit/Framework/Fixture.php
@@ -152,12 +152,15 @@ class Fixture extends \PHPUnit_Framework_Assert
     {
         if ($this->createConfig) {
             IniSettingsProvider::unsetSingletonInstance();
-            Config::setSingletonInstance(new TestConfig());
         }
 
         $this->piwikEnvironment = new Environment('test');
         $this->piwikEnvironment->init();
 
+        if ($this->createConfig) {
+            Config::setSingletonInstance(new TestConfig());
+        }
+
         try {
             $this->dbName = $this->getDbName();
 
diff --git a/tests/PHPUnit/Framework/Mock/TestConfig.php b/tests/PHPUnit/Framework/Mock/TestConfig.php
index ebdb6e473a..4feb2b3e58 100644
--- a/tests/PHPUnit/Framework/Mock/TestConfig.php
+++ b/tests/PHPUnit/Framework/Mock/TestConfig.php
@@ -25,16 +25,16 @@ class TestConfig extends Config
         $this->allowSave = $allowSave;
         $this->doSetTestEnvironment = $doSetTestEnvironment;
 
-        $this->reload();
+        $this->reload($pathGlobal, $pathLocal, $pathCommon);
     }
 
-    public function reload()
+    public function reload($pathLocal = null, $pathGlobal = null, $pathCommon = null)
     {
         if ($this->isSettingTestEnv) {
-            parent::reload();
+            parent::reload($pathGlobal, $pathLocal, $pathCommon);
         } else {
             $this->isSettingTestEnv = true;
-            $this->setTestEnvironment($this->getLocalPath(), $this->getGlobalPath(), $this->getCommonPath(), $this->allowSave);
+            $this->setTestEnvironment($pathLocal, $pathGlobal, $pathCommon, $this->allowSave);
             $this->isSettingTestEnv = false;
         }
     }
diff --git a/tests/PHPUnit/System/EnvironmentValidationTest.php b/tests/PHPUnit/System/EnvironmentValidationTest.php
new file mode 100644
index 0000000000..e80cb43822
--- /dev/null
+++ b/tests/PHPUnit/System/EnvironmentValidationTest.php
@@ -0,0 +1,222 @@
+<?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\System;
+
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\SystemTestCase;
+
+/**
+ * @group System
+ */
+class EnvironmentValidationTest extends SystemTestCase
+{
+    public function getEntryPointsToTest()
+    {
+        return array(
+            array('tracker'),
+            array('web'),
+            array('console')
+        );
+    }
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $testingEnvironment = new \Piwik_TestingEnvironment();
+        $testingEnvironment->configFileGlobal = null;
+        $testingEnvironment->configFileLocal = null;
+        $testingEnvironment->configFileCommon = null;
+        $testingEnvironment->save();
+    }
+
+    /**
+     * @dataProvider getEntryPointsToTest
+     */
+    public function test_NoGlobalConfigFile_TriggersError($entryPoint)
+    {
+        $this->simulateAbsentConfigFile('global.ini.php');
+
+        $output = $this->triggerPiwikFrom($entryPoint);
+        $this->assertOutputContainsConfigFileMissingError('global.ini.php', $output);
+    }
+
+    public function getEntryPointsThatErrorWithNoLocal()
+    {
+        return array(
+            array('tracker'),
+            array('console')
+        );
+    }
+
+    /**
+     * @dataProvider getEntryPointsThatErrorWithNoLocal
+     */
+    public function test_NoLocalConfigFile_TriggersError($entryPoint)
+    {
+        $this->simulateAbsentConfigFile('config.ini.php');
+
+        $output = $this->triggerPiwikFrom($entryPoint);
+        $this->assertOutputContainsConfigFileMissingError('config.ini.php', $output);
+    }
+
+    public function test_NoLocalConfigFile_StartsInstallation_PiwikAccessedThroughWeb()
+    {
+        $this->simulateAbsentConfigFile('config.ini.php');
+
+        $output = $this->triggerPiwikFrom('web');
+        $this->assertInstallationProcessStarted($output);
+    }
+
+    public function getEntryPointsAndConfigFilesToTest()
+    {
+        return array(
+            array('global.ini.php', 'tracker'),
+            array('global.ini.php', 'web'),
+            array('global.ini.php', 'console'),
+
+            array('config.ini.php', 'tracker'),
+            array('config.ini.php', 'web'),
+            array('config.ini.php', 'console'),
+
+            array('common.config.ini.php', 'tracker'),
+            array('common.config.ini.php', 'web'),
+            array('common.config.ini.php', 'console'),
+        );
+    }
+
+    /**
+     * @dataProvider getEntryPointsAndConfigFilesToTest
+     */
+    public function test_BadConfigFile_TriggersError($configFile, $entryPoint)
+    {
+        $this->simulateBadConfigFile($configFile);
+
+        $output = $this->triggerPiwikFrom($entryPoint);
+        $this->assertOutputContainsConfigFileMissingError($configFile, $output);
+    }
+
+    /**
+     * @dataProvider getEntryPointsToTest
+     */
+    public function test_BadDomainSpecificLocalConfigFile_TriggersError($entryPoint)
+    {
+        $this->simulateHost('piwik.kobra.org');
+
+        $configFile = 'piwik.kobra.org.config.ini.php';
+        $this->simulateBadConfigFile($configFile);
+
+        $output = $this->triggerPiwikFrom($entryPoint);
+        $this->assertOutputContainsBadConfigFileError($output);
+    }
+
+    private function assertOutputContainsConfigFileMissingError($fileName, $output)
+    {
+        // TODO: need to tweak error message displayed.
+        $this->assertRegExp("/The configuration file \\{.*\\/" . preg_quote($fileName) . "\\} has not been found or could not be read\\./", $output);
+    }
+
+    private function assertOutputContainsBadConfigFileError($output)
+    {
+        // TODO: also mention bad INI format possible
+        $this->assertRegExp("/The configuration file \\{.*\\/piwik.php\\} could not be read\\. Your host may have disabled parse_ini_file\\(\\)/", $output);
+    }
+
+    private function assertInstallationProcessStarted($output)
+    {
+        $this->assertRegExp('<div id="installationPage">', $output);
+    }
+
+    private function simulateAbsentConfigFile($fileName)
+    {
+        $testingEnvironment = new \Piwik_TestingEnvironment();
+
+        if ($fileName == 'global.ini.php') {
+            $testingEnvironment->configFileGlobal = PIWIK_INCLUDE_PATH . '/tmp/nonexistant/global.ini.php';
+        } else if ($fileName == 'common.config.ini.php') {
+            $testingEnvironment->configFileCommon = PIWIK_INCLUDE_PATH . '/tmp/nonexistant/common.config.ini.php';
+        } else {
+            $testingEnvironment->configFileLocal = PIWIK_INCLUDE_PATH . '/tmp/nonexistant/' . $fileName;
+        }
+
+        $testingEnvironment->save();
+    }
+
+    private function simulateBadConfigFile($fileName)
+    {
+        $testingEnvironment = new \Piwik_TestingEnvironment();
+
+        if ($fileName == 'global.ini.php') {
+            $testingEnvironment->configFileGlobal = PIWIK_INCLUDE_PATH . '/piwik.php';
+        } else if ($fileName == 'common.config.ini.php') {
+            $testingEnvironment->configFileCommon = PIWIK_INCLUDE_PATH . '/piwik.php';
+        } else {
+            $testingEnvironment->configFileLocal = PIWIK_INCLUDE_PATH . '/piwik.php';
+        }
+
+        $testingEnvironment->save();
+    }
+
+    private function simulateHost($host)
+    {
+        $testingEnvironment = new \Piwik_TestingEnvironment();
+        $testingEnvironment->hostOverride = $host;
+        $testingEnvironment->save();
+    }
+
+    private function triggerPiwikFrom($entryPoint)
+    {
+        if ($entryPoint == 'tracker') {
+            return $this->sendRequestToTracker();
+        } else if ($entryPoint == 'web') {
+            return $this->sendRequestToWeb();
+        } else if ($entryPoint == 'console') {
+            return $this->startConsoleProcess();
+        } else {
+            throw new \Exception("Don't know how to access '$entryPoint'.");
+        }
+    }
+
+    private function sendRequestToTracker()
+    {
+        return $this->curl(Fixture::getRootUrl() . 'tests/PHPUnit/proxy/piwik.php?idsite=1&rec=1&action_name=something');
+    }
+
+    private function sendRequestToWeb()
+    {
+        return $this->curl(Fixture::getRootUrl() . 'tests/PHPUnit/proxy/index.php');
+    }
+
+    private function startConsoleProcess()
+    {
+        $pathToProxyConsole = PIWIK_INCLUDE_PATH . '/tests/PHPUnit/proxy/console';
+        return shell_exec("php '$pathToProxyConsole' list 2>&1");
+    }
+
+    private function curl($url)
+    {
+        if (!function_exists('curl_init')) {
+            $this->markTestSkipped('Curl is not installed');
+        }
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HEADER, true);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+
+        $response = curl_exec($ch);
+        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+        $response = substr($response, $headerSize);
+
+        curl_close($ch);
+
+        return $response;
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/TestingEnvironment.php b/tests/PHPUnit/TestingEnvironment.php
index afd398c01b..dc0b6adf77 100644
--- a/tests/PHPUnit/TestingEnvironment.php
+++ b/tests/PHPUnit/TestingEnvironment.php
@@ -102,7 +102,10 @@ class Piwik_TestingEnvironment
 
     public function getCoreAndSupportedPlugins()
     {
-        $disabledPlugins = PluginManager::getInstance()->getCorePluginsDisabledByDefault();
+        $settings = new \Piwik\Application\Kernel\GlobalSettingsProvider\IniSettingsProvider();
+        $pluginManager = new PluginManager(new \Piwik\Application\Kernel\PluginList\IniPluginList($settings));
+
+        $disabledPlugins = $pluginManager->getCorePluginsDisabledByDefault();
         $disabledPlugins[] = 'LoginHttpAuth';
         $disabledPlugins[] = 'ExampleVisualization';
 
@@ -110,13 +113,13 @@ class Piwik_TestingEnvironment
             'DBStats', 'ExampleUI', 'ExampleCommand', 'ExampleSettingsPlugin'
         ));
 
-        $plugins = array_filter(PluginManager::getInstance()->readPluginsDirectory(), function ($pluginName) use ($disabledPlugins) {
+        $plugins = array_filter($pluginManager->readPluginsDirectory(), function ($pluginName) use ($disabledPlugins, $pluginManager) {
             if (in_array($pluginName, $disabledPlugins)) {
                 return false;
             }
 
-            return PluginManager::getInstance()->isPluginBundledWithCore($pluginName)
-                || PluginManager::getInstance()->isPluginOfficialAndNotBundledWithCore($pluginName);
+            return $pluginManager->isPluginBundledWithCore($pluginName)
+                || $pluginManager->isPluginOfficialAndNotBundledWithCore($pluginName);
         });
 
         sort($plugins);
@@ -148,25 +151,27 @@ class Piwik_TestingEnvironment
             \Piwik\Profiler::setupProfilerXHProf($mainRun = false, $setupDuringTracking = true);
         }
 
-        if ($testingEnvironment->dontUseTestConfig) {
-            Config::setSingletonInstance(new Config(
-                $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
-            ));
-        }
+        \Piwik\Application\Kernel\GlobalSettingsProvider\IniSettingsProvider::getSingletonInstance(
+            $testingEnvironment->configFileGlobal,
+            $testingEnvironment->configFileLocal,
+            $testingEnvironment->configFileCommon
+        );
 
         // Apply DI config from the fixture
+        $diConfig = array();
         if ($testingEnvironment->fixtureClass) {
             $fixtureClass = $testingEnvironment->fixtureClass;
             if (class_exists($fixtureClass)) {
                 /** @var Fixture $fixture */
                 $fixture = new $fixtureClass;
                 $diConfig = $fixture->provideContainerConfig();
-                if (!empty($diConfig)) {
-                    StaticContainer::addDefinitions($diConfig);
-                }
             }
         }
 
+        if (!empty($diConfig)) {
+            StaticContainer::addDefinitions($diConfig);
+        }
+
         \Piwik\Cache\Backend\File::$invalidateOpCacheBeforeRead = true;
 
         Piwik::addAction('Access.createAccessSingleton', function($access) use ($testingEnvironment) {
@@ -175,8 +180,16 @@ class Piwik_TestingEnvironment
                 \Piwik\Access::setSingletonInstance($access);
             }
         });
+
+        $pluginsToLoad = $testingEnvironment->getCoreAndSupportedPlugins();
+        if (!empty($testingEnvironment->pluginsToLoad)) {
+            $pluginsToLoad = array_unique(array_merge($pluginsToLoad, $testingEnvironment->pluginsToLoad));
+        }
+
+        sort($pluginsToLoad);
+
         if (!$testingEnvironment->dontUseTestConfig) {
-            Piwik::addAction('Config.createConfigSingleton', function(IniFileChain $chain) use ($testingEnvironment) {
+            Piwik::addAction('Config.createConfigSingleton', function(IniFileChain $chain) use ($testingEnvironment, $pluginsToLoad) {
                 $general =& $chain->get('General');
                 $plugins =& $chain->get('Plugins');
                 $log =& $chain->get('log');
@@ -186,14 +199,6 @@ class Piwik_TestingEnvironment
                     $general['session_save_handler'] = 'dbtable';
                 }
 
-                $manager = \Piwik\Plugin\Manager::getInstance();
-                $pluginsToLoad = $testingEnvironment->getCoreAndSupportedPlugins();
-                if (!empty($testingEnvironment->pluginsToLoad)) {
-                    $pluginsToLoad = array_unique(array_merge($pluginsToLoad, $testingEnvironment->pluginsToLoad));
-                }
-
-                sort($pluginsToLoad);
-
                 $plugins['Plugins'] = $pluginsToLoad;
 
                 $log['log_writers'] = array('file');
@@ -216,15 +221,27 @@ class Piwik_TestingEnvironment
             Config::setSingletonInstance(new TestConfig(
                 $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
             ));
+        } else {
+            Config::setSingletonInstance(new Config(
+                $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon
+            ));
         }
         Piwik::addAction('Request.dispatch', function() use ($testingEnvironment) {
             if (empty($_GET['ignoreClearAllViewDataTableParameters'])) { // TODO: should use testingEnvironment variable, not query param
-                \Piwik\ViewDataTable\Manager::clearAllViewDataTableParameters();
+                try {
+                    \Piwik\ViewDataTable\Manager::clearAllViewDataTableParameters();
+                } catch (\Exception $ex) {
+                    // ignore (in case DB is not setup)
+                }
             }
 
             if ($testingEnvironment->optionsOverride) {
-                foreach ($testingEnvironment->optionsOverride as $name => $value) {
-                    Option::set($name, $value);
+                try {
+                    foreach ($testingEnvironment->optionsOverride as $name => $value) {
+                        Option::set($name, $value);
+                    }
+                } catch (\Exception $ex) {
+                    // ignore (in case DB is not setup)
                 }
             }
 
diff --git a/tests/PHPUnit/proxy/console b/tests/PHPUnit/proxy/console
new file mode 100644
index 0000000000..1ccef19ecc
--- /dev/null
+++ b/tests/PHPUnit/proxy/console
@@ -0,0 +1,7 @@
+#!/usr/bin/env php
+<?php
+require realpath(dirname(__FILE__)) . "/includes.php";
+
+Piwik_TestingEnvironment::addHooks();
+
+require_once PIWIK_INCLUDE_PATH . "/console";
-- 
GitLab