# 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
from robot.errors import DataError
from robot.output import LOGGER
from robot.parsing import SuiteStructureBuilder, SuiteStructureVisitor
from .parsers import RobotParser, NoInitFileDirectoryParser, RestParser
from .testsettings import TestDefaults
[docs]class TestSuiteBuilder(object):
"""Builder to construct ``TestSuite`` objects based on data on the disk.
The :meth:`build` method constructs executable
:class:`~robot.running.model.TestSuite` objects based on test data files
or directories. There are two main use cases for this API:
- Execute the created suite by using its
:meth:`~robot.running.model.TestSuite.run` method. The suite can be
can be modified before execution if needed.
- Inspect the suite to see, for example, what tests it has or what tags
tests have. This can be more convenient than using the lower level
:mod:`~robot.parsing` APIs but does not allow saving modified data
back to the disk.
Both modifying the suite and inspecting what data it contains are easiest
done by using the :mod:`~robot.model.visitor` interface.
This class is part of the public API and should be imported via the
:mod:`robot.api` package.
"""
def __init__(self, included_suites=None, included_extensions=('robot',),
rpa=None, allow_empty_suite=False, process_curdir=True):
"""
:param include_suites:
List of suite names to include. If ``None`` or an empty list,
all suites are included. Same as using :option:`--suite` on
the command line.
:param included_extensions:
List of extensions of files to parse. Same as :option:`--extension`.
This parameter was named ``extension`` before RF 3.2.
: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.
Same as :option:`--rpa` or :option:`--norpa`.
:param allow_empty_suite:
Specify is it an error if the built suite contains no tests.
Same as :option:`--runemptysuite`. New in RF 3.2.
:param process_curdir:
Control processing the special ``${CURDIR}`` variable. It is
resolved already at parsing time by default, but that can be
changed by giving this argument ``False`` value. New in RF 3.2.
"""
self.rpa = rpa
self.included_suites = included_suites
self.included_extensions = included_extensions
self.allow_empty_suite = allow_empty_suite
self.process_curdir = process_curdir
[docs] def build(self, *paths):
"""
:param paths: Paths to test data files or directories.
:return: :class:`~robot.running.model.TestSuite` instance.
"""
structure = SuiteStructureBuilder(self.included_extensions,
self.included_suites).build(paths)
parser = SuiteStructureParser(self.included_extensions,
self.rpa, self.process_curdir)
suite = parser.parse(structure)
if not self.included_suites and not self.allow_empty_suite:
self._validate_test_counts(suite, multisource=len(paths) > 1)
suite.remove_empty_suites(preserve_direct_children=len(paths) > 1)
return suite
def _validate_test_counts(self, suite, multisource=False):
def validate(suite):
if not suite.has_tests:
raise DataError("Suite '%s' contains no tests or tasks."
% suite.name)
if not multisource:
validate(suite)
else:
for s in suite.suites:
validate(s)
[docs]class SuiteStructureParser(SuiteStructureVisitor):
def __init__(self, included_extensions, rpa=None, process_curdir=True):
self.rpa = rpa
self._rpa_given = rpa is not None
self.suite = None
self._stack = []
self.parsers = self._get_parsers(included_extensions, process_curdir)
def _get_parsers(self, extensions, process_curdir):
robot_parser = RobotParser(process_curdir)
rest_parser = RestParser(process_curdir)
parsers = {
None: NoInitFileDirectoryParser(),
'robot': robot_parser,
'rst': rest_parser,
'rest': rest_parser
}
for ext in extensions:
if ext not in parsers:
parsers[ext] = robot_parser
return parsers
def _get_parser(self, extension):
try:
return self.parsers[extension]
except KeyError:
return self.parsers['robot']
[docs] def parse(self, structure):
structure.visit(self)
self.suite.rpa = self.rpa
return self.suite
[docs] def visit_file(self, structure):
LOGGER.info("Parsing file '%s'." % structure.source)
suite, _ = self._build_suite(structure)
if self._stack:
self._stack[-1][0].suites.append(suite)
else:
self.suite = suite
[docs] def start_directory(self, structure):
if structure.source:
LOGGER.info("Parsing directory '%s'." % structure.source)
suite, defaults = self._build_suite(structure)
if self.suite is None:
self.suite = suite
else:
self._stack[-1][0].suites.append(suite)
self._stack.append((suite, defaults))
[docs] def end_directory(self, structure):
suite, _ = self._stack.pop()
if suite.rpa is None and suite.suites:
suite.rpa = suite.suites[0].rpa
def _build_suite(self, structure):
parent_defaults = self._stack[-1][-1] if self._stack else None
source = structure.source
defaults = TestDefaults(parent_defaults)
parser = self._get_parser(structure.extension)
try:
if structure.is_directory:
suite = parser.parse_init_file(structure.init_file or source, defaults)
else:
suite = parser.parse_suite_file(source, defaults)
if not suite.tests:
LOGGER.info("Data source '%s' has no tests or tasks." % source)
self._validate_execution_mode(suite)
except DataError as err:
raise DataError("Parsing '%s' failed: %s" % (source, err.message))
return suite, defaults
def _validate_execution_mode(self, suite):
if self._rpa_given:
suite.rpa = self.rpa
elif suite.rpa is None:
pass
elif self.rpa is None:
self.rpa = suite.rpa
elif self.rpa is not suite.rpa:
this, that = ('tasks', 'tests') if suite.rpa else ('tests', 'tasks')
raise DataError("Conflicting execution modes. File has %s "
"but files parsed earlier have %s. Fix headers "
"or use '--rpa' or '--norpa' options to set the "
"execution mode explicitly." % (this, that))
[docs]class ResourceFileBuilder(object):
def __init__(self, process_curdir=True):
self.process_curdir = process_curdir
[docs] def build(self, source):
LOGGER.info("Parsing resource file '%s'." % source)
resource = self._parse(source)
if resource.imports or resource.variables or resource.keywords:
LOGGER.info("Imported resource file '%s' (%d keywords)."
% (source, len(resource.keywords)))
else:
LOGGER.warn("Imported resource file '%s' is empty." % source)
return resource
def _parse(self, source):
if os.path.splitext(source)[1].lower() in ('.rst', '.rest'):
return RestParser(self.process_curdir).parse_resource_file(source)
return RobotParser(self.process_curdir).parse_resource_file(source)