# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os.path
import warnings
from robot.errors import DataError
from robot.output import LOGGER
from robot.parsing import TestData, ResourceFile as ResourceData, TEST_EXTENSIONS
from robot.running.defaults import TestDefaults
from robot.utils import abspath, is_string, normalize, unic
from robot.variables import VariableIterator
from .model import ForLoop, Keyword, ResourceFile, TestSuite
[docs]class TestSuiteBuilder(object):
"""Creates executable :class:`~robot.running.model.TestSuite` objects.
Suites are build based on existing test data on the file system.
See the overall documentation of the :mod:`robot.running` package for
more information and examples.
"""
def __init__(self, include_suites=None, warn_on_skipped='DEPRECATED',
extension=None, rpa=None):
"""
:param include_suites: List of suite names to include. If ``None`` or
an empty list, all suites are included. When executing tests
normally, these names are specified using the ``--suite`` option.
:param warn_on_skipped: Deprecated.
:param extension: Limit parsing test data to only these files. Files
are specified as an extension that is handled case-insensitively.
Same as ``--extension`` on the command line.
:param rpa: Explicit test execution mode. ``True`` for RPA and
``False`` for test automation. By default mode is got from test
data headers and possible conflicting headers cause an error.
"""
self.include_suites = include_suites
self.extensions = self._get_extensions(extension)
builder = StepBuilder()
self._build_steps = builder.build_steps
self._build_step = builder.build_step
self.rpa = rpa
self._rpa_not_given = rpa is None
# TODO: Remove in RF 3.2.
if warn_on_skipped != 'DEPRECATED':
warnings.warn("Option 'warn_on_skipped' is deprecated and has no "
"effect.", DeprecationWarning)
def _get_extensions(self, extension):
if not extension:
return None
extensions = set(ext.lower().lstrip('.') for ext in extension.split(':'))
if not all(ext in TEST_EXTENSIONS for ext in extensions):
raise DataError("Invalid extension to limit parsing '%s'." % extension)
return extensions
[docs] def build(self, *paths):
"""
:param paths: Paths to test data files or directories.
:return: :class:`~robot.running.model.TestSuite` instance.
"""
if not paths:
raise DataError('One or more source paths required.')
if len(paths) == 1:
return self._parse_and_build(paths[0])
root = TestSuite()
for path in paths:
root.suites.append(self._parse_and_build(path))
root.rpa = self.rpa
return root
def _parse_and_build(self, path):
suite = self._build_suite(self._parse(path))
suite.remove_empty_suites()
return suite
def _parse(self, path):
try:
return TestData(source=abspath(path),
include_suites=self.include_suites,
extensions=self.extensions)
except DataError as err:
raise DataError("Parsing '%s' failed: %s" % (path, err.message))
def _build_suite(self, data, parent_defaults=None):
if self._rpa_not_given and data.testcase_table.is_started():
self._set_execution_mode(data)
self._check_deprecated_extensions(data.source)
defaults = TestDefaults(data.setting_table, parent_defaults)
suite = TestSuite(name=data.name,
source=data.source,
doc=unic(data.setting_table.doc),
metadata=self._get_metadata(data.setting_table))
self._build_setup(suite, data.setting_table.suite_setup)
self._build_teardown(suite, data.setting_table.suite_teardown)
for test_data in data.testcase_table.tests:
self._build_test(suite, test_data, defaults)
for child in data.children:
suite.suites.append(self._build_suite(child, defaults))
suite.rpa = self.rpa
ResourceFileBuilder().build(data, target=suite.resource)
return suite
def _set_execution_mode(self, data):
rpa = normalize(data.testcase_table.header[0]) in ('task', 'tasks')
if self.rpa is None:
self.rpa = rpa
elif self.rpa is not rpa:
this, that = ('tasks', 'tests') if rpa else ('tests', 'tasks')
raise DataError("Conflicting execution modes. File '%s' has %s "
"but files parsed earlier have %s. Fix headers "
"or use '--rpa' or '--norpa' options to set the "
"execution mode explicitly."
% (data.source, this, that))
def _check_deprecated_extensions(self, source):
if os.path.isdir(source):
return
ext = os.path.splitext(source)[1][1:].lower()
if self.extensions and ext in self.extensions:
return
# HTML files cause deprecation warning that cannot be avoided with
# --extension at parsing time. No need for double warning.
if ext not in ('robot', 'html', 'htm', 'xhtml'):
LOGGER.warn("Automatically parsing other than '*.robot' files is "
"deprecated. Convert '%s' to '*.robot' format or use "
"'--extension' to explicitly configure which files to "
"parse." % source)
def _get_metadata(self, settings):
# Must return as a list to preserve ordering
return [(meta.name, meta.value) for meta in settings.metadata]
def _build_test(self, suite, data, defaults):
values = defaults.get_test_values(data)
template = self._get_template(values.template)
test = suite.tests.create(name=data.name,
doc=unic(data.doc),
tags=values.tags.value,
template=template,
timeout=self._get_timeout(values.timeout))
self._build_setup(test, values.setup)
self._build_steps(test, data, template)
self._build_teardown(test, values.teardown)
def _get_timeout(self, timeout):
return (timeout.value, timeout.message) if timeout else None
def _get_template(self, template):
return unic(template) if template.is_active() else None
def _build_setup(self, parent, data):
if data.is_active():
self._build_step(parent, data, kw_type='setup')
def _build_teardown(self, parent, data):
if data.is_active():
self._build_step(parent, data, kw_type='teardown')
[docs]class ResourceFileBuilder(object):
def __init__(self):
builder = StepBuilder()
self._build_steps = builder.build_steps
self._build_step = builder.build_step
[docs] def build(self, path_or_data, target=None):
data, source = self._import_resource_if_needed(path_or_data)
if not target:
target = ResourceFile(doc=data.setting_table.doc.value, source=source)
self._build_imports(target, data.setting_table.imports)
self._build_variables(target, data.variable_table.variables)
for kw_data in data.keyword_table.keywords:
self._build_keyword(target, kw_data)
return target
def _import_resource_if_needed(self, path_or_data):
if not is_string(path_or_data):
return path_or_data, path_or_data.source
return ResourceData(path_or_data).populate(), path_or_data
def _build_imports(self, target, imports):
for data in imports:
target.imports.create(type=data.type,
name=data.name,
args=tuple(data.args),
alias=data.alias)
def _build_variables(self, target, variables):
for data in variables:
if data:
target.variables.create(name=data.name, value=data.value)
def _build_keyword(self, target, data):
kw = target.keywords.create(name=data.name,
args=tuple(data.args),
doc=unic(data.doc),
tags=tuple(data.tags),
return_=tuple(data.return_),
timeout=self._get_timeout(data.timeout))
self._build_steps(kw, data)
if data.teardown.is_active():
self._build_step(kw, data.teardown, kw_type='teardown')
def _get_timeout(self, timeout):
return (timeout.value, timeout.message) if timeout else None
[docs]class StepBuilder(object):
[docs] def build_steps(self, parent, data, template=None, kw_type='kw'):
steps = [self._build(step, template, kw_type) for step in data.steps
if step and not step.is_comment()]
parent.keywords.extend(steps)
[docs] def build_step(self, parent, data, template=None, kw_type='kw'):
if data and not data.is_comment():
step = self._build(data, template, kw_type)
parent.keywords.append(step)
def _build(self, data, template=None, kw_type='kw'):
if data.is_for_loop():
return self._build_for_loop(data, template)
if template:
return self._build_templated_step(data, template)
return self._build_normal_step(data, kw_type)
def _build_for_loop(self, data, template):
loop = ForLoop(variables=data.vars,
values=data.items,
flavor=data.flavor)
self.build_steps(loop, data, template)
return loop
def _build_templated_step(self, data, template):
args = data.as_list(include_comment=False)
template, args = self._format_template(template, args)
return Keyword(name=template, args=args)
def _format_template(self, template, args):
iterator = VariableIterator(template, identifiers='$')
variables = len(iterator)
if not variables or variables != len(args):
return template, tuple(args)
temp = []
for before, variable, after in iterator:
temp.extend([before, args.pop(0)])
temp.append(after)
return ''.join(temp), ()
def _build_normal_step(self, data, kw_type):
return Keyword(name=data.name,
args=tuple(data.args),
assign=tuple(data.assign),
type=kw_type)