From 99b418a7c5a16c4c487fba8d88e2c4379241dc40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= <rgonalo@gmail.com>
Date: Wed, 3 Nov 2021 10:38:42 +0100
Subject: [PATCH] feat: add toolium system properties (#247)

* feat: add toolium system properties

* underscore is allowed in options
---
 CHANGELOG.rst                                 |  25 ++-
 docs/driver_configuration.rst                 |  58 ++++++-
 docs/remote_configuration.rst                 |   4 +-
 docs/tests_result_analysis.rst                |  18 ++-
 toolium/behave/environment.py                 |  22 +--
 toolium/config_parser.py                      |  32 +++-
 toolium/driver_wrapper.py                     |  14 +-
 toolium/driver_wrappers_pool.py               |  22 ++-
 toolium/selenoid.py                           |   1 +
 toolium/test/behave/test_environment.py       |  10 +-
 toolium/test/test_config_parser.py            | 142 ++++++++++++++++--
 toolium/test/test_driver_wrapper.py           |  45 ++++--
 toolium/test/test_driver_wrapper_logger.py    |  36 +++--
 .../test/test_driver_wrapper_properties.py    |  64 +++++---
 toolium/test/test_driver_wrappers_pool.py     |  12 +-
 15 files changed, 400 insertions(+), 105 deletions(-)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 97451353..4106554e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,8 +7,27 @@ v2.2.0
 *Release date: In development*
 
 - Add JSON object/list conversion to Python dict/list in the type inference logic of the *replace_param* function
-- Add *finalize_properties_configuration* method in *DriverWrapper* class to allow the modification of config properties upon initialization
-  programmatically before driver creation
+- Add *finalize_properties_configuration* method in *DriverWrapper* class to allow the modification of config properties
+  upon initialization programmatically before driver creation
+- Properties values configured by properties files can be overridden with system properties named
+  *TOOLIUM_[SECTION]_[OPTION]*, moreover these system properties can be used to add new properties that do not exist in
+  properties files
+- Configuration system properties have been renamed. The old property names are deprecated but they can still be used.
+
+   | Deprecated property name -> New property name
+   | Config_environment -> TOOLIUM_CONFIG_ENVIRONMENT
+   | Output_directory -> TOOLIUM_OUTPUT_DIRECTORY
+   | Output_log_filename -> TOOLIUM_OUTPUT_LOG_FILENAME
+   | Config_directory -> TOOLIUM_CONFIG_DIRECTORY
+   | Config_log_filename -> TOOLIUM_CONFIG_LOG_FILENAME
+   | Config_prop_filenames -> TOOLIUM_CONFIG_PROPERTIES_FILENAMES
+   | Visual_baseline_directory -> TOOLIUM_VISUAL_BASELINE_DIRECTORY
+
+- Behave user property 'Config_environment' is deprecated, use 'TOOLIUM_CONFIG_ENVIRONMENT' instead:
+
+.. code:: console
+
+    $ behave -D TOOLIUM_CONFIG_ENVIRONMENT=android
 
 v2.1.1
 ------
@@ -545,7 +564,7 @@ v0.10.0
 
    New config section [ChromeMobileEmulation] with mobile emulation options, e.g. 'deviceName = Google Nexus 5'
 
-- Configuration system properties has been renamed
+- Configuration system properties have been renamed
 
    | Old properties: Files_output_path, Files_log_filename, Files_properties, Files_logging
    | New properties: Output_directory, Output_log_filename, Config_directory, Config_prop_filenames, Config_log_filename
diff --git a/docs/driver_configuration.rst b/docs/driver_configuration.rst
index b5aa7853..3c65ceb0 100644
--- a/docs/driver_configuration.rst
+++ b/docs/driver_configuration.rst
@@ -20,27 +20,75 @@ If driver is not needed, typically in API tests, disable it using an empty strin
 
 By default, Toolium configuration is loaded from :code:`conf/properties.cfg` and :code:`conf/local-properties.cfg` files. If
 different properties files are used for different environments, they can be selected using a system property named
-:code:`Config_environment`. For example, if :code:`Config_environment` value is :code:`android`, Toolium configuration
-will be loaded from :code:`conf/properties.cfg`, :code:`conf/android-properties.cfg` and
+:code:`TOOLIUM_CONFIG_ENVIRONMENT`. For example, if :code:`TOOLIUM_CONFIG_ENVIRONMENT` value is :code:`android`,
+Toolium configuration will be loaded from :code:`conf/properties.cfg`, :code:`conf/android-properties.cfg` and
 :code:`local-android-properties.cfg` files:
 
 Nose:
 
 .. code:: console
 
-    $ Config_environment=android nosetests web/tests/test_web.py
+    $ TOOLIUM_CONFIG_ENVIRONMENT=android nosetests web/tests/test_web.py
 
 Pytest:
 
 .. code:: console
 
-    $ Config_environment=android pytest web_pytest/tests/test_web_pytest.py
+    $ TOOLIUM_CONFIG_ENVIRONMENT=android pytest web_pytest/tests/test_web_pytest.py
 
 Behave:
 
 .. code:: console
 
-    $ behave -D Config_environment=android
+    $ behave -D TOOLIUM_CONFIG_ENVIRONMENT=android
+
+Modify configuration by system properties
+-----------------------------------------
+
+Properties values configured by properties files can be overridden with system properties. To modify a particular option
+within a section, a new system variable should be defined. The property name must be `TOOLIUM_[SECTION]_[OPTION]` and
+its value must be `[Section]_[option]=value`, as can be seen in the following example:
+
+.. code:: console
+
+    $ TOOLIUM_DRIVER_TYPE=Driver_type=chrome
+
+This system property means the same as having the following section in the configuration file::
+
+    [Driver]
+    type: chrome
+
+Underscore is allowed in options, but not in sections, for instance:
+
+.. code:: console
+
+    $ TOOLIUM_SERVER_VIDEO_ENABLED=Server_video_enabled=true
+
+This is the same as having the following section in the configuration file::
+
+    [Server]
+    video_enabled: true
+
+To be cross-platform, section and option must be configured both in the property name and in the first token of the
+value because they are case sensitive and, in Windows, system properties names are case insensitive.
+
+Modify configuration programmatically
+-------------------------------------
+
+Properties values can also be modified programmatically before Toolium uses them to start the driver. There is a method
+named `finalize_properties_configuration`, called after reading configuration files and before starting the driver, that
+can be monkey patched to modify properties values, for instance:
+
+.. code:: python
+
+    from toolium.driver_wrapper import DriverWrapper
+
+    def finalize_properties_configuration(self):
+        if self.config.getboolean_optional('Server', 'enabled'):
+            self.config.set('Capabilities', 'enableVideo', 'true'):
+
+    DriverWrapper.finalize_properties_configuration = finalize_properties_configuration
+
 
 - :ref:`Browser Configuration <browser_configuration>`
 - :ref:`Mobile Configuration <mobile_configuration>`
diff --git a/docs/remote_configuration.rst b/docs/remote_configuration.rst
index c8e1c3ea..ffb737db 100644
--- a/docs/remote_configuration.rst
+++ b/docs/remote_configuration.rst
@@ -52,8 +52,8 @@ video_enabled
 | `GGR with Selenoid <https://github.com/aerokube/ggr>`_ as remote server, that allow recording videos of test
 | executions.
 |
-| *true*: remote video recording is enabled, a video of the test execution will be recorded and saved locally
-| *false*: remote video recording is disabled
+| *true*: remote video download is enabled even if the test passes, the test execution video will be saved locally
+| *false*: remote video download is disabled in passed tests, but it is always enabled if the test fails
 
 logs_enabled
 ~~~~~~~~~~~~
diff --git a/docs/tests_result_analysis.rst b/docs/tests_result_analysis.rst
index 3e278c92..da6cec00 100644
--- a/docs/tests_result_analysis.rst
+++ b/docs/tests_result_analysis.rst
@@ -41,7 +41,12 @@ Videos
 When a test fails, Toolium downloads a video of the test execution into the folder *output/videos/DATE_DRIVER_TYPE*.
 This feature only works when the test has been executed in a
 `Selenium Grid Extras <https://github.com/groupon/Selenium-Grid-Extras>`_ or `GGR <https://github.com/aerokube/ggr>`_
-grid node. Both grids allow recording videos of test executions.
+grid node and the video recording has been enabled in grid. Both grids allow recording videos of test executions.
+
+For example, to enable video recording in GGR, the following capability must be configured ::
+
+    [Capabilities]
+    enableVideo: true
 
 In order to download the execution video even if the test passes, configure the property *video_enabled* in *[Server]*
 section in properties.cfg file ::
@@ -49,12 +54,6 @@ section in properties.cfg file ::
     [Server]
     video_enabled: true
 
-video_enabled
-~~~~~~~~~~~~~
-| *true*: remote video recording is enabled, a video of the test execution will be recorded and saved locally
-| *false*: remote video recording is disabled
-
-
 Webdriver logs
 --------------
 
@@ -74,6 +73,11 @@ Notice that if using Chrome, log types can be enabled in *capabilities* section
     [Capabilities]
     goog___loggingPrefs: {'browser':'ALL', 'driver': 'ALL', 'performance': 'ALL'}
 
+Also take into account that, to enable logs in GGR, the following capability must be configured ::
+
+    [Capabilities]
+    enableLog: true
+
 In order to download webdriver logs even if the test passes, configure the property `logs_enabled <https://toolium.readthedocs.io/en/latest/remote_configuration.html#logs-enabled>`_
 in *[Server]* section in properties.cfg file ::
 
diff --git a/toolium/behave/environment.py b/toolium/behave/environment.py
index 032bc260..d2c8c966 100644
--- a/toolium/behave/environment.py
+++ b/toolium/behave/environment.py
@@ -42,12 +42,14 @@ def before_all(context):
     # Use pytest asserts if behave_pytest is installed
     install_pytest_asserts()
 
-    # Get 'Config_environment' property from user input (e.g. -D Config_environment=ios)
-    env = context.config.userdata.get('Config_environment')
-    # Deprecated: Get 'env' property from user input (e.g. -D env=ios)
+    # Get 'TOOLIUM_CONFIG_ENVIRONMENT' property from user input (e.g. -D TOOLIUM_CONFIG_ENVIRONMENT=ios)
+    env = context.config.userdata.get('TOOLIUM_CONFIG_ENVIRONMENT')
+    # Deprecated: use TOOLIUM_CONFIG_ENVIRONMENT property
+    env = env if env else context.config.userdata.get('Config_environment')
+    # Deprecated: use TOOLIUM_CONFIG_ENVIRONMENT property
     env = env if env else context.config.userdata.get('env')
     if env:
-        os.environ['Config_environment'] = env
+        os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] = env
 
     if not hasattr(context, 'config_files'):
         context.config_files = ConfigFiles()
@@ -91,14 +93,14 @@ def before_scenario(context, scenario):
     """
     # Configure reset properties from behave tags
     if 'no_reset_app' in scenario.tags:
-        os.environ["AppiumCapabilities_noReset"] = 'true'
-        os.environ["AppiumCapabilities_fullReset"] = 'false'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_NORESET'] = 'AppiumCapabilities_noReset=true'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_FULLRESET'] = 'AppiumCapabilities_fullReset=false'
     elif 'reset_app' in scenario.tags:
-        os.environ["AppiumCapabilities_noReset"] = 'false'
-        os.environ["AppiumCapabilities_fullReset"] = 'false'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_NORESET'] = 'AppiumCapabilities_noReset=false'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_FULLRESET'] = 'AppiumCapabilities_fullReset=false'
     elif 'full_reset_app' in scenario.tags:
-        os.environ["AppiumCapabilities_noReset"] = 'false'
-        os.environ["AppiumCapabilities_fullReset"] = 'true'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_NORESET'] = 'AppiumCapabilities_noReset=false'
+        os.environ['TOOLIUM_APPIUMCAPABILITIES_FULLRESET'] = 'AppiumCapabilities_fullReset=true'
 
     # Force to reset driver before each scenario if it has @reset_driver tag
     if 'reset_driver' in scenario.tags:
diff --git a/toolium/config_parser.py b/toolium/config_parser.py
index b7a2aff4..748420e3 100644
--- a/toolium/config_parser.py
+++ b/toolium/config_parser.py
@@ -17,13 +17,16 @@
 """
 
 import logging
+import re
 from configparser import ConfigParser, NoSectionError, NoOptionError
 from io import StringIO
 
+logger = logging.getLogger(__name__)
+
 
 class ExtendedConfigParser(ConfigParser):
     def optionxform(self, optionstr):
-        """Override default optionxform in ConfigParser"""
+        """Override default optionxform in ConfigParser to allow case sensitive options"""
         return optionstr
 
     def get_optional(self, section, option, default=None):
@@ -95,6 +98,33 @@ def _update_property_from_dict(self, section, option, new_properties):
         except KeyError:
             pass
 
+    def update_toolium_system_properties(self, new_properties):
+        """ Update config properties values or add new values if property does not exist
+        Property name must be 'TOOLIUM_[SECTION]_[OPTION]' and property value must be '[Section]_[option]=value'
+        i.e. TOOLIUM_SERVER_ENABLED='Server_enabled=true'
+
+        Section and option can not be configured in property name because they must be case sensitive and, in Windows,
+        system properties are case insensitive
+
+        :param new_properties: dict with new properties values
+        """
+        for property_name, property_value in new_properties.items():
+            name_groups = re.match('^TOOLIUM_([^_]+)_(.+)$', property_name)
+            value_groups = re.match('^([^_=]+)_([^=]+)=(.*)$', property_value)
+            if (name_groups and value_groups
+                    and name_groups.group(1).upper() == value_groups.group(1).upper()
+                    and name_groups.group(2).upper() == value_groups.group(2).upper()):
+                section = value_groups.group(1)
+                option = value_groups.group(2)
+                value = value_groups.group(3)
+                if not self.has_section(section):
+                    self.add_section(section)
+                self.set(section, option, value)
+            elif property_name.startswith('TOOLIUM'):
+                logger.warning('A toolium system property is configured but its name does not math with section'
+                               ' and option in value (use TOOLIUM_[SECTION]_[OPTION]=[Section]_[option]=value):'
+                               ' %s=%s' % (property_name, property_value))
+
     def translate_config_variables(self, str_with_variables):
         """
         Translate config variables included in string with format {Section_option}
diff --git a/toolium/driver_wrapper.py b/toolium/driver_wrapper.py
index f728fdc3..70d38cd7 100644
--- a/toolium/driver_wrapper.py
+++ b/toolium/driver_wrapper.py
@@ -85,14 +85,16 @@ def configure_logger(self, tc_config_log_filename=None, tc_output_log_filename=N
         :param tc_output_log_filename: test case specific output logger file
         """
         # Get config logger filename
-        config_log_filename = DriverWrappersPool.get_configured_value('Config_log_filename', tc_config_log_filename,
+        config_log_filename = DriverWrappersPool.get_configured_value('TOOLIUM_CONFIG_LOG_FILENAME',
+                                                                      'Config_log_filename', tc_config_log_filename,
                                                                       'logging.conf')
         config_log_filename = os.path.join(DriverWrappersPool.config_directory, config_log_filename)
 
         # Configure logger only if logging filename has changed
         if self.config_log_filename != config_log_filename:
             # Get output logger filename
-            output_log_filename = DriverWrappersPool.get_configured_value('Output_log_filename', tc_output_log_filename,
+            output_log_filename = DriverWrappersPool.get_configured_value('TOOLIUM_OUTPUT_LOG_FILENAME',
+                                                                          'Output_log_filename', tc_output_log_filename,
                                                                           'toolium.log')
             output_log_filename = os.path.join(DriverWrappersPool.output_directory, output_log_filename)
             output_log_filename = output_log_filename.replace('\\', '\\\\')
@@ -111,7 +113,8 @@ def configure_properties(self, tc_config_prop_filenames=None, behave_properties=
         :param tc_config_prop_filenames: test case specific properties filenames
         :param behave_properties: dict with behave user data properties
         """
-        prop_filenames = DriverWrappersPool.get_configured_value('Config_prop_filenames', tc_config_prop_filenames,
+        prop_filenames = DriverWrappersPool.get_configured_value('TOOLIUM_CONFIG_PROPERTIES_FILENAMES',
+                                                                 'Config_prop_filenames', tc_config_prop_filenames,
                                                                  'properties.cfg;local-properties.cfg')
         prop_filenames = [os.path.join(DriverWrappersPool.config_directory, filename) for filename in
                           prop_filenames.split(';')]
@@ -123,9 +126,12 @@ def configure_properties(self, tc_config_prop_filenames=None, behave_properties=
             self.config = ExtendedConfigParser.get_config_from_file(prop_filenames)
             self.config_properties_filenames = prop_filenames
 
-        # Override properties with system properties
+        # Override properties with system properties [Deprecated: use toolium system properties]
         self.config.update_properties(os.environ)
 
+        # Override properties with toolium system properties
+        self.config.update_toolium_system_properties(os.environ)
+
         # Override properties with behave userdata properties
         if behave_properties:
             self.config.update_properties(behave_properties)
diff --git a/toolium/driver_wrappers_pool.py b/toolium/driver_wrappers_pool.py
index 3a9be6cf..e44ef9a0 100644
--- a/toolium/driver_wrappers_pool.py
+++ b/toolium/driver_wrappers_pool.py
@@ -254,10 +254,11 @@ def save_all_webdriver_or_ggr_logs(cls, test_name, test_passed, ggr=False):
             driver_index += 1
 
     @staticmethod
-    def get_configured_value(system_property_name, specific_value, default_value):
+    def get_configured_value(system_property_name, deprecated_system_property_name, specific_value, default_value):
         """Get configured value from system properties, method parameters or default value
 
         :param system_property_name: system property name
+        :param deprecated_system_property_name: deprecated system property name
         :param specific_value: test case specific value
         :param default_value: default value
         :returns: configured value
@@ -265,7 +266,10 @@ def get_configured_value(system_property_name, specific_value, default_value):
         try:
             return os.environ[system_property_name]
         except KeyError:
-            return specific_value if specific_value else default_value
+            try:
+                return os.environ[deprecated_system_property_name]
+            except KeyError:
+                return specific_value if specific_value else default_value
 
     @classmethod
     def configure_common_directories(cls, tc_config_files):
@@ -275,14 +279,15 @@ def configure_common_directories(cls, tc_config_files):
         """
         if cls.config_directory is None:
             # Get config directory from properties
-            config_directory = cls.get_configured_value('Config_directory', tc_config_files.config_directory, 'conf')
-            prop_filenames = cls.get_configured_value('Config_prop_filenames',
+            config_directory = cls.get_configured_value('TOOLIUM_CONFIG_DIRECTORY', 'Config_directory',
+                                                        tc_config_files.config_directory, 'conf')
+            prop_filenames = cls.get_configured_value('TOOLIUM_CONFIG_PROPERTIES_FILENAMES', 'Config_prop_filenames',
                                                       tc_config_files.config_properties_filenames, 'properties.cfg')
             cls.config_directory = cls._find_parent_directory(config_directory, prop_filenames.split(';')[0])
 
             # Get output directory from properties and create it
-            cls.output_directory = cls.get_configured_value('Output_directory', tc_config_files.output_directory,
-                                                            'output')
+            cls.output_directory = cls.get_configured_value('TOOLIUM_OUTPUT_DIRECTORY', 'Output_directory',
+                                                            tc_config_files.output_directory, 'output')
             if not os.path.isabs(cls.output_directory):
                 # If output directory is relative, we use the same path as config directory
                 cls.output_directory = os.path.join(os.path.dirname(cls.config_directory), cls.output_directory)
@@ -290,7 +295,8 @@ def configure_common_directories(cls, tc_config_files):
 
             # Get visual baseline directory from properties
             default_baseline = os.path.join(cls.output_directory, 'visualtests', 'baseline')
-            cls.visual_baseline_directory = cls.get_configured_value('Visual_baseline_directory',
+            cls.visual_baseline_directory = cls.get_configured_value('TOOLIUM_VISUAL_BASELINE_DIRECTORY',
+                                                                     'Visual_baseline_directory',
                                                                      tc_config_files.visual_baseline_directory,
                                                                      default_baseline)
             if not os.path.isabs(cls.visual_baseline_directory):
@@ -361,7 +367,7 @@ def initialize_config_files(tc_config_files=None):
             tc_config_files = ConfigFiles()
 
         # Update properties and log file names if an environment is configured
-        env = DriverWrappersPool.get_configured_value('Config_environment', None, None)
+        env = DriverWrappersPool.get_configured_value('TOOLIUM_CONFIG_ENVIRONMENT', 'Config_environment', None, None)
         if env:
             # Update config properties filenames
             prop_filenames = tc_config_files.config_properties_filenames
diff --git a/toolium/selenoid.py b/toolium/selenoid.py
index 880c4d70..c791734e 100644
--- a/toolium/selenoid.py
+++ b/toolium/selenoid.py
@@ -38,6 +38,7 @@ class Selenoid(object):
     # capabilities to selenoid
     enableVideo: true
     enableVNC: true
+    enableLog: true
 
     [Server]
     enabled: true            --> MANDATORY
diff --git a/toolium/test/behave/test_environment.py b/toolium/test/behave/test_environment.py
index 8dc0344f..085739b4 100644
--- a/toolium/test/behave/test_environment.py
+++ b/toolium/test/behave/test_environment.py
@@ -66,8 +66,9 @@ def test_before_all(create_and_configure_wrapper):
 
 
 properties = (
-    'env',
+    'TOOLIUM_CONFIG_ENVIRONMENT',
     'Config_environment',
+    'env'
 )
 
 
@@ -81,13 +82,14 @@ def test_before_all_config_environment(create_and_configure_wrapper, property_na
 
     before_all(context)
 
-    # Check that configuration folder is the same as environment folder and property 'Config_environment' is configured
+    # Check that configuration folder is the same as environment folder and property 'TOOLIUM_CONFIG_ENVIRONMENT' is
+    # configured
     expected_config_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'conf')
     assert context.config_files.config_directory == expected_config_directory
     assert context.config_files.config_properties_filenames == 'properties.cfg;os-properties.cfg;local-os-properties.cfg'
     assert context.config_files.config_log_filename is None
-    assert os.environ['Config_environment'] == 'os'
-    del os.environ["Config_environment"]
+    assert os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] == 'os'
+    del os.environ['TOOLIUM_CONFIG_ENVIRONMENT']
 
 
 @mock.patch('toolium.behave.environment.start_driver')
diff --git a/toolium/test/test_config_parser.py b/toolium/test/test_config_parser.py
index 483e26a3..49eb91e2 100644
--- a/toolium/test/test_config_parser.py
+++ b/toolium/test/test_config_parser.py
@@ -16,11 +16,46 @@
 limitations under the License.
 """
 
+import mock
 import os
 import pytest
 
 from toolium.config_parser import ExtendedConfigParser
 
+environment_properties = []
+
+
+@pytest.fixture
+def config():
+    root_path = os.path.dirname(os.path.realpath(__file__))
+    conf_properties_file = os.path.join(root_path, 'conf', 'properties.cfg')
+    config = ExtendedConfigParser()
+    config.read(conf_properties_file)
+
+    yield config
+
+    # Remove used environment properties after test
+    global environment_properties
+    for env_property in environment_properties:
+        try:
+            del os.environ[env_property]
+        except KeyError:
+            pass
+    environment_properties = []
+
+
+@pytest.fixture
+def logger():
+    # Configure logger mock
+    logger = mock.MagicMock()
+    logger_patch = mock.patch('toolium.config_parser.logger', logger)
+    logger_patch.start()
+
+    yield logger
+
+    logger_patch.stop()
+
+
 optional_values = (
     ('No section', 'No option', None, None),
     ('No section', 'No option', 'chrome', 'chrome'),
@@ -40,15 +75,6 @@
 )
 
 
-@pytest.fixture
-def config():
-    root_path = os.path.dirname(os.path.realpath(__file__))
-    conf_properties_file = os.path.join(root_path, 'conf', 'properties.cfg')
-    config = ExtendedConfigParser()
-    config.read(conf_properties_file)
-    return config
-
-
 @pytest.mark.parametrize("section, option, default, response", optional_values)
 def test_get_optional(section, option, default, response, config):
     if default:
@@ -172,7 +198,7 @@ def test_deepcopy_and_modify_option_with_colon(config):
     assert new_value == new_config.get(section, option)
 
 
-def test_update_properties_environ(config):
+def test_update_properties_environ_deprecated(config):
     section = 'AppiumCapabilities'
     option = 'platformName'
     orig_value = 'Android'
@@ -189,6 +215,102 @@ def test_update_properties_environ(config):
     assert new_value == config.get(section, option)
 
 
+toolium_system_properties = (
+    # Update value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', 'iOS'),
+    # Underscore in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', 'a_b'),
+    # Equal symbol in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', 'a=b'),
+    # Empty value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', ''),
+    # Underscore in option
+    ('TOOLIUM_SERVER_VIDEO_ENABLED', 'Server', 'video_enabled', 'false', 'true'),
+    # New section
+    ('TOOLIUM_CUSTOMCAPABILITIES_CUSTOMCAPABILITY', 'CustomCapabilities', 'customCapability', None, 'prueba'),
+    # New option
+    ('TOOLIUM_CAPABILITIES_CUSTOMCAPABILITY', 'Capabilities', 'customCapability', None, 'prueba'),
+    # Lowercase section in name
+    ('TOOLIUM_APPIUMCapabilities_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', 'iOS'),
+    # Lowercase option in name
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMName', 'AppiumCapabilities', 'platformName', 'Android', 'iOS'),
+)
+
+
+@pytest.mark.parametrize("property_name, section, option, value, new_value", toolium_system_properties)
+def test_update_toolium_system_properties(config, property_name, section, option, value, new_value):
+    # Check previous value
+    if value is None:
+        assert not config.has_option(section, option)
+    else:
+        assert value == config.get(section, option)
+
+    # Change system property and update config
+    environment_properties.append(property_name)
+    os.environ[property_name] = '{}_{}={}'.format(section, option, new_value)
+    config.update_toolium_system_properties(os.environ)
+
+    # Check the new config value
+    assert new_value == config.get(section, option)
+
+
+toolium_system_properties_wrong_format = (
+    # No section and option in name
+    ('TOOLIUM', 'AppiumCapabilities', 'platformName', 'Android', 'AppiumCapabilities_platformName=iOS'),
+    # No option in name
+    ('TOOLIUM_APPIUMCAPABILITIES', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities_platformName=iOS'),
+    # Option in name different from option in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORM', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities_platformName=iOS'),
+    # Additional param in name
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME_WRONG', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities_platformName=iOS'),
+    # No option in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities=iOS'),
+    # Additional param in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities_platformName_wrong=iOS'),
+    # No equal in value
+    ('TOOLIUM_APPIUMCAPABILITIES_PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android',
+     'AppiumCapabilities_platformName iOS'),
+    # Empty section
+    ('TOOLIUM__PLATFORMNAME', 'AppiumCapabilities', 'platformName', 'Android', '_platformName=iOS'),
+    # Empty option
+    ('TOOLIUM_APPIUMCAPABILITIES_', 'AppiumCapabilities', 'platformName', 'Android', 'AppiumCapabilities_=iOS'),
+    # No toolium system property
+    ('WRONGNAME', 'AppiumCapabilities', 'platformName', 'Android', 'platformName_Android=iOS'),
+)
+
+
+@pytest.mark.parametrize("property_name, section, option, value, property_value",
+                         toolium_system_properties_wrong_format)
+def test_update_toolium_system_properties_wrong_format(config, logger, property_name, section, option, value,
+                                                       property_value):
+    # Check previous value
+    if value is None:
+        assert not config.has_option(section, option)
+    else:
+        assert value == config.get(section, option)
+
+    # Change system property and update config
+    environment_properties.append(property_name)
+    os.environ[property_name] = property_value
+    config.update_toolium_system_properties(os.environ)
+
+    # Check that config value has not changed
+    assert value == config.get(section, option)
+
+    # Check logging error message
+    if property_name.startswith('TOOLIUM'):
+        logger.warning.assert_called_once_with('A toolium system property is configured but its name does not math with'
+                                               ' section and option in value (use TOOLIUM_[SECTION]_[OPTION]=[Section]_'
+                                               '[option]=value): %s=%s' % (property_name, property_value))
+    else:
+        logger.warning.assert_not_called()
+
+
 def test_update_properties_behave(config):
     section = 'AppiumCapabilities'
     option = 'platformName'
diff --git a/toolium/test/test_driver_wrapper.py b/toolium/test/test_driver_wrapper.py
index 4e5c66d5..81bf725d 100644
--- a/toolium/test/test_driver_wrapper.py
+++ b/toolium/test/test_driver_wrapper.py
@@ -38,8 +38,7 @@
 
 # (driver_type, appium_app, appium_browser_name, is_web, is_android_web, is_ios_web)
 web_tests = (
-    ('android-4.1.2-on-android', 'C:/Demo.apk', None, False, False,
-     False),
+    ('android-4.1.2-on-android', 'C:/Demo.apk', None, False, False, False),
     ('android', 'C:/Demo.apk', None, False, False, False),
     ('android', 'C:/Demo.apk', '', False, False, False),
     ('android', None, 'chrome', True, True, False),
@@ -67,6 +66,8 @@
     ('iphone', False),
 )
 
+environment_properties = []
+
 
 @pytest.fixture
 def driver_wrapper():
@@ -87,15 +88,14 @@ def driver_wrapper():
 
     yield new_driver_wrapper
 
-    # Remove environment properties after test
-    try:
-        del os.environ["Config_prop_filenames"]
-    except KeyError:
-        pass
-    try:
-        del os.environ["Config_environment"]
-    except KeyError:
-        pass
+    # Remove used environment properties after test
+    global environment_properties
+    for env_property in environment_properties:
+        try:
+            del os.environ[env_property]
+        except KeyError:
+            pass
+    environment_properties = []
 
 
 def test_multiple(driver_wrapper):
@@ -136,7 +136,8 @@ def test_configure_change_configuration_file(driver_wrapper):
 
     # Change properties file and try to configure again
     root_path = os.path.dirname(os.path.realpath(__file__))
-    os.environ["Config_prop_filenames"] = os.path.join(root_path, 'conf', 'android-properties.cfg')
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = os.path.join(root_path, 'conf', 'android-properties.cfg')
     driver_wrapper.configure(ConfigFiles())
 
     # Check that configuration has been initialized
@@ -148,7 +149,22 @@ def test_configure_environment(driver_wrapper):
     assert driver_wrapper.config.get('Driver', 'type') == 'firefox'
 
     # Change environment and try to configure again
-    os.environ["Config_environment"] = 'android'
+    environment_properties.append('TOOLIUM_CONFIG_ENVIRONMENT')
+    os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] = 'android'
+    config_files = DriverWrappersPool.initialize_config_files(ConfigFiles())
+    driver_wrapper.configure(config_files)
+
+    # Check that configuration has been initialized
+    assert driver_wrapper.config.get('Driver', 'type') == 'android'
+
+
+def test_configure_environment_deprecated_property(driver_wrapper):
+    # Check previous values
+    assert driver_wrapper.config.get('Driver', 'type') == 'firefox'
+
+    # Change environment and try to configure again
+    environment_properties.append('Config_environment')
+    os.environ['Config_environment'] = 'android'
     config_files = DriverWrappersPool.initialize_config_files(ConfigFiles())
     driver_wrapper.configure(config_files)
 
@@ -200,7 +216,8 @@ def test_connect_api_from_file(driver_wrapper):
 
     # Change driver type to api and configure again
     root_path = os.path.dirname(os.path.realpath(__file__))
-    os.environ["Config_prop_filenames"] = os.path.join(root_path, 'conf', 'api-properties.cfg')
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = os.path.join(root_path, 'conf', 'api-properties.cfg')
     driver_wrapper.configure(ConfigFiles())
 
     # Connect and check that the returned driver is None
diff --git a/toolium/test/test_driver_wrapper_logger.py b/toolium/test/test_driver_wrapper_logger.py
index b1c257fe..347515e0 100644
--- a/toolium/test/test_driver_wrapper_logger.py
+++ b/toolium/test/test_driver_wrapper_logger.py
@@ -24,6 +24,8 @@
 from toolium.config_files import ConfigFiles
 from toolium.driver_wrappers_pool import DriverWrappersPool
 
+environment_properties = []
+
 
 @pytest.fixture
 def driver_wrapper():
@@ -39,15 +41,26 @@ def driver_wrapper():
 
     yield new_driver_wrapper
 
-    # Remove environment properties after test
-    try:
-        del os.environ["Config_log_filename"]
-    except KeyError:
-        pass
+    # Remove used environment properties after test
+    global environment_properties
+    for env_property in environment_properties:
+        try:
+            del os.environ[env_property]
+        except KeyError:
+            pass
+    environment_properties = []
+
+
+def test_configure_logger_deprecated(driver_wrapper):
+    environment_properties.append('Config_log_filename')
+    os.environ['Config_log_filename'] = 'logging.conf'
+    driver_wrapper.configure_logger()
+    assert logging.getLevelName(driver_wrapper.logger.getEffectiveLevel()) == 'DEBUG'
 
 
 def test_configure_logger(driver_wrapper):
-    os.environ["Config_log_filename"] = 'logging.conf'
+    environment_properties.append('TOOLIUM_CONFIG_LOG_FILENAME')
+    os.environ['TOOLIUM_CONFIG_LOG_FILENAME'] = 'logging.conf'
     driver_wrapper.configure_logger()
     assert logging.getLevelName(driver_wrapper.logger.getEffectiveLevel()) == 'DEBUG'
 
@@ -58,14 +71,16 @@ def test_configure_logger_file_not_configured(driver_wrapper):
 
 
 def test_configure_logger_file_not_found(driver_wrapper):
-    os.environ["Config_log_filename"] = 'notfound-logging.conf'
+    environment_properties.append('TOOLIUM_CONFIG_LOG_FILENAME')
+    os.environ['TOOLIUM_CONFIG_LOG_FILENAME'] = 'notfound-logging.conf'
     driver_wrapper.configure_logger()
     driver_wrapper.logger.info('No error, default logging configuration')
 
 
 def test_configure_logger_no_changes(driver_wrapper):
     # Configure logger
-    os.environ["Config_log_filename"] = 'logging.conf'
+    environment_properties.append('TOOLIUM_CONFIG_LOG_FILENAME')
+    os.environ['TOOLIUM_CONFIG_LOG_FILENAME'] = 'logging.conf'
     driver_wrapper.configure_logger()
     logger = logging.getLogger('selenium.webdriver.remote.remote_connection')
 
@@ -83,7 +98,8 @@ def test_configure_logger_no_changes(driver_wrapper):
 
 def test_configure_logger_change_configuration_file(driver_wrapper):
     # Configure logger
-    os.environ["Config_log_filename"] = 'logging.conf'
+    environment_properties.append('TOOLIUM_CONFIG_LOG_FILENAME')
+    os.environ['TOOLIUM_CONFIG_LOG_FILENAME'] = 'logging.conf'
     logger = logging.getLogger('selenium.webdriver.remote.remote_connection')
 
     # Modify property
@@ -92,7 +108,7 @@ def test_configure_logger_change_configuration_file(driver_wrapper):
     assert logging.getLevelName(logger.level) == new_level
 
     # Change file and try to configure again
-    os.environ["Config_log_filename"] = 'warn-logging.conf'
+    os.environ['TOOLIUM_CONFIG_LOG_FILENAME'] = 'warn-logging.conf'
     driver_wrapper.configure_logger()
 
     # Check that configuration has been initialized
diff --git a/toolium/test/test_driver_wrapper_properties.py b/toolium/test/test_driver_wrapper_properties.py
index 1b5311f3..5b4f6d33 100644
--- a/toolium/test/test_driver_wrapper_properties.py
+++ b/toolium/test/test_driver_wrapper_properties.py
@@ -24,6 +24,8 @@
 from toolium.driver_wrapper import DriverWrapper
 from toolium.driver_wrappers_pool import DriverWrappersPool
 
+environment_properties = []
+
 
 @pytest.fixture
 def driver_wrapper():
@@ -45,19 +47,28 @@ def driver_wrapper():
 
     yield new_driver_wrapper
 
-    # Remove environment properties after test
-    try:
-        del os.environ["Config_prop_filenames"]
-    except KeyError:
-        pass
-    try:
-        del os.environ["Driver_type"]
-    except KeyError:
-        pass
+    # Remove used environment properties after test
+    global environment_properties
+    for env_property in environment_properties:
+        try:
+            del os.environ[env_property]
+        except KeyError:
+            pass
+    environment_properties = []
+
+
+def test_configure_properties_deprecated(driver_wrapper):
+    environment_properties.append('Config_prop_filenames')
+    os.environ['Config_prop_filenames'] = 'properties.cfg'
+    driver_wrapper.configure_properties()
+    assert driver_wrapper.config.get('Driver', 'type') == 'firefox'  # get last value
+    assert driver_wrapper.config.get_optional('Driver', 'implicitly_wait') == '5'  # only in properties
+    assert driver_wrapper.config.get_optional('AppiumCapabilities', 'app') is None  # only in android
 
 
 def test_configure_properties(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'firefox'  # get last value
     assert driver_wrapper.config.get_optional('Driver', 'implicitly_wait') == '5'  # only in properties
@@ -65,7 +76,8 @@ def test_configure_properties(driver_wrapper):
 
 
 def test_configure_properties_android(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'android-properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'android-properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'android'  # get last value
     assert driver_wrapper.config.get_optional('Driver', 'implicitly_wait') is None  # only in properties
@@ -74,7 +86,8 @@ def test_configure_properties_android(driver_wrapper):
 
 
 def test_configure_properties_two_files(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'properties.cfg;android-properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg;android-properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'android'  # get last value
     assert driver_wrapper.config.get_optional('Driver', 'implicitly_wait') == '5'  # only in properties
@@ -83,7 +96,8 @@ def test_configure_properties_two_files(driver_wrapper):
 
 
 def test_configure_properties_two_files_android_first(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'android-properties.cfg;properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'android-properties.cfg;properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'firefox'  # get last value
     assert driver_wrapper.config.get_optional('Driver', 'implicitly_wait') == '5'  # only in properties
@@ -92,8 +106,10 @@ def test_configure_properties_two_files_android_first(driver_wrapper):
 
 
 def test_configure_properties_system_property(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'properties.cfg'
-    os.environ["Driver_type"] = 'opera'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    environment_properties.append('TOOLIUM_DRIVER_TYPE')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg'
+    os.environ['TOOLIUM_DRIVER_TYPE'] = 'Driver_type=opera'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'opera'
 
@@ -104,7 +120,8 @@ def test_configure_properties_file_default_file(driver_wrapper):
 
 
 def test_configure_properties_file_not_found(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'notfound-properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'notfound-properties.cfg'
     with pytest.raises(Exception) as excinfo:
         driver_wrapper.configure_properties()
     assert 'Properties config file not found' in str(excinfo.value)
@@ -112,7 +129,8 @@ def test_configure_properties_file_not_found(driver_wrapper):
 
 def test_configure_properties_no_changes(driver_wrapper):
     # Configure properties
-    os.environ["Config_prop_filenames"] = 'properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'firefox'
 
@@ -130,7 +148,8 @@ def test_configure_properties_no_changes(driver_wrapper):
 
 def test_configure_properties_change_configuration_file(driver_wrapper):
     # Configure properties
-    os.environ["Config_prop_filenames"] = 'properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg'
     driver_wrapper.configure_properties()
     assert driver_wrapper.config.get('Driver', 'type') == 'firefox'
 
@@ -140,7 +159,8 @@ def test_configure_properties_change_configuration_file(driver_wrapper):
     assert driver_wrapper.config.get('Driver', 'type') == new_driver_type
 
     # Change file and try to configure again
-    os.environ["Config_prop_filenames"] = 'android-properties.cfg'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'android-properties.cfg'
     driver_wrapper.configure_properties()
 
     # Check that configuration has been initialized
@@ -148,8 +168,10 @@ def test_configure_properties_change_configuration_file(driver_wrapper):
 
 
 def test_configure_properties_finalize_properties(driver_wrapper):
-    os.environ["Config_prop_filenames"] = 'properties.cfg'
-    os.environ["Driver_type"] = 'opera'
+    environment_properties.append('TOOLIUM_CONFIG_PROPERTIES_FILENAMES')
+    environment_properties.append('TOOLIUM_DRIVER_TYPE')
+    os.environ['TOOLIUM_CONFIG_PROPERTIES_FILENAMES'] = 'properties.cfg'
+    os.environ['TOOLIUM_DRIVER_TYPE'] = 'Driver_type=opera'
 
     # Modify properties after reading properties file and system properties
     def finalize_properties_configuration(self):
diff --git a/toolium/test/test_driver_wrappers_pool.py b/toolium/test/test_driver_wrappers_pool.py
index 153090eb..70656e36 100644
--- a/toolium/test/test_driver_wrappers_pool.py
+++ b/toolium/test/test_driver_wrappers_pool.py
@@ -159,11 +159,11 @@ def test_initialize_config_files_new():
 
 def test_initialize_config_files_new_environment():
     config_files = None
-    os.environ["Config_environment"] = 'android'
+    os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] = 'android'
 
     # Initialize config files
     config_files = DriverWrappersPool.initialize_config_files(config_files)
-    del os.environ["Config_environment"]
+    del os.environ['TOOLIUM_CONFIG_ENVIRONMENT']
 
     # Check expected config files
     expected_properties_filenames = 'properties.cfg;android-properties.cfg;local-android-properties.cfg'
@@ -188,11 +188,11 @@ def test_initialize_config_files_configured_environment():
     config_files = ConfigFiles()
     config_files.set_config_properties_filenames('test.conf', 'local-test.conf')
     config_files.set_output_log_filename('test.log')
-    os.environ["Config_environment"] = 'android'
+    os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] = 'android'
 
     # Initialize config files
     config_files = DriverWrappersPool.initialize_config_files(config_files)
-    del os.environ["Config_environment"]
+    del os.environ['TOOLIUM_CONFIG_ENVIRONMENT']
 
     # Check expected config files
     expected_properties_filenames = 'test.conf;local-test.conf;android-test.conf;local-android-test.conf'
@@ -204,11 +204,11 @@ def test_initialize_config_files_configured_environment_with_points():
     config_files = ConfigFiles()
     config_files.set_config_properties_filenames('test.new.conf', 'local-test.new.conf')
     config_files.set_output_log_filename('test.new.log')
-    os.environ["Config_environment"] = 'ios'
+    os.environ['TOOLIUM_CONFIG_ENVIRONMENT'] = 'ios'
 
     # Initialize config files
     config_files = DriverWrappersPool.initialize_config_files(config_files)
-    del os.environ["Config_environment"]
+    del os.environ['TOOLIUM_CONFIG_ENVIRONMENT']
 
     # Check expected config files
     expected_properties_filenames = 'test.new.conf;local-test.new.conf;ios-test.new.conf;local-ios-test.new.conf'