# 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 .settings import Defaults
[docs]class TestSuiteBuilder:
"""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
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, lang=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 `--suite` on the command line.
:param included_extensions:
List of extensions of files to parse. Same as `--extension`.
:param rpa: Explicit test execution mode. ``True`` for RPA and
``False`` for test automation. By default, mode is got from data file
headers and possible conflicting headers cause an error.
Same as `--rpa` or `--norpa`.
:param lang: Additional languages to be supported during parsing.
Can be a string matching any of the supported language codes or names,
an initialized :class:`~robot.conf.languages.Language` subsclass,
a list containing such strings or instances, or a
:class:`~robot.conf.languages.Languages` instance.
:param allow_empty_suite:
Specify is it an error if the built suite contains no tests.
Same as `--runemptysuite`.
: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.
"""
self.rpa = rpa
self.lang = lang
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.lang, 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(f"Suite '{suite.name}' contains no tests or tasks.")
if not multisource:
validate(suite)
else:
for s in suite.suites:
validate(s)
[docs]class SuiteStructureParser(SuiteStructureVisitor):
def __init__(self, included_extensions, rpa=None, lang=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, lang, process_curdir)
def _get_parsers(self, extensions, lang, process_curdir):
robot_parser = RobotParser(lang, process_curdir)
rest_parser = RestParser(lang, 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(f"Parsing file '{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(f"Parsing directory '{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 = Defaults(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(f"Data source '{source}' has no tests or tasks.")
self._validate_execution_mode(suite)
except DataError as err:
raise DataError(f"Parsing '{source}' failed: {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(f"Conflicting execution modes. File has {this} "
f"but files parsed earlier have {that}. Fix headers "
f"or use '--rpa' or '--norpa' options to set the "
f"execution mode explicitly.")
[docs]class ResourceFileBuilder:
def __init__(self, lang=None, process_curdir=True):
self.lang = lang
self.process_curdir = process_curdir
[docs] def build(self, source):
LOGGER.info(f"Parsing resource file '{source}'.")
resource = self._parse(source)
if resource.imports or resource.variables or resource.keywords:
LOGGER.info(f"Imported resource file '{source}' ({len(resource.keywords)} "
f"keywords).")
else:
LOGGER.warn(f"Imported resource file '{source}' is empty.")
return resource
def _parse(self, source):
if os.path.splitext(source)[1].lower() in ('.rst', '.rest'):
return RestParser(self.lang, self.process_curdir).parse_resource_file(source)
return RobotParser(self.lang, self.process_curdir).parse_resource_file(source)