Skip to content

Commit

Permalink
feat: add toolium system properties (#247)
Browse files Browse the repository at this point in the history
* feat: add toolium system properties

* underscore is allowed in options
  • Loading branch information
rgonalo authored Nov 3, 2021
1 parent 3921cf9 commit 99b418a
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 105 deletions.
25 changes: 22 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------
Expand Down Expand Up @@ -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
Expand Down
58 changes: 53 additions & 5 deletions docs/driver_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`
Expand Down
4 changes: 2 additions & 2 deletions docs/remote_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~
Expand Down
18 changes: 11 additions & 7 deletions docs/tests_result_analysis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,19 @@ 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 ::

[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
--------------

Expand All @@ -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 ::

Expand Down
22 changes: 12 additions & 10 deletions toolium/behave/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 31 additions & 1 deletion toolium/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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}
Expand Down
14 changes: 10 additions & 4 deletions toolium/driver_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('\\', '\\\\')
Expand All @@ -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(';')]
Expand All @@ -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)
Expand Down
22 changes: 14 additions & 8 deletions toolium/driver_wrappers_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,18 +254,22 @@ 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
"""
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):
Expand All @@ -275,22 +279,24 @@ 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)
makedirs_safe(cls.output_directory)

# 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):
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions toolium/selenoid.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Selenoid(object):
# capabilities to selenoid
enableVideo: true
enableVNC: true
enableLog: true
[Server]
enabled: true --> MANDATORY
Expand Down
Loading

0 comments on commit 99b418a

Please sign in to comment.