robot-frame的application 流程
0、argumentparser
ArgumentParser
ArgLimitValidator
ArgFileParser
1、
robot.utils.application:
Application
execute_cli
options, arguments = self._parse_arguments(cli_arguments)
rc = self._execute(arguments, options)
2、
robot.RobotFramework(Application):
Application.init
def main(self, datasources, **options):
settings = RobotSettings(options)
LOGGER.register_console_logger(**settings.console_output_config)
LOGGER.info('Settings:\n%s' % unic(settings))
builder = TestSuiteBuilder(settings['SuiteNames'],
extension=settings.extension,
rpa=settings.rpa)
suite = builder.build(*datasources)
settings.rpa = builder.rpa
suite.configure(**settings.suite_config)
if settings.pre_run_modifiers:
suite.visit(ModelModifier(settings.pre_run_modifiers,
settings.run_empty_suite, LOGGER))
**** testsuit: visitor.visit_suite(self)
with pyloggingconf.robot_handler_enabled(settings.log_level):
old_max_error_lines = text.MAX_ERROR_LINES
text.MAX_ERROR_LINES = settings.max_error_lines
try:
result = suite.run(settings)
****model.py TestSuite
finally:
text.MAX_ERROR_LINES = old_max_error_lines
LOGGER.info("Tests execution ended. Statistics:\n%s"
% result.suite.stat_message)
if settings.log or settings.report or settings.xunit:
writer = ResultWriter(settings.output if settings.log
else result)
writer.write_results(settings.get_rebot_settings())
return result.return_code
run_cli
run: RobotFramework().execute(*tests, **options)
if __name__ == '__main__':
run_cli(sys.argv[1:])
3、
robot.robot:
Rebot(RobotFramework):
Application.__init__
main: RebotSettings ResultWriter
rebot_cli: Rebot().execute_cli(arguments, exit=exit)
rebot: Rebot().execute(*outputs, **options)
if __name__ == '__main__':
rebot_cli(sys.argv[1:])
4、
robot.running.builder:
TestSuiteBuilder:
__init__
builder = StepBuilder()
self._build_steps = builder.build_steps
self._build_step = builder.build_step
build
root = TestSuite()
_parse_and_build
_build_suite
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)
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)
for child in data.children:
suite.suites.append(self._build_suite(child, defaults))
suite.rpa = self.rpa
ResourceFileBuilder().build(data, target=suite.resource)
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')
ResourceFileBuilder:
__init__
builder = StepBuilder()
self._build_steps = builder.build_steps
self._build_step = builder.build_step
build:
_import_resource_if_needed
ResourceData(path_or_data).populate(), path_or_data
_build_imports
target.imports.create(type=data.type,
name=data.name,
args=tuple(data.args),
alias=data.alias)
_build_variables
target.variables.create(name=data.name, value=data.value)
_build_keyword
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))
(timeout.value, timeout.message) if timeout else None
self._build_steps(kw, data)
if data.teardown.is_active():
self._build_step(kw, data.teardown, kw_type='teardown')
StepBuilder:
build_steps
build_step
_build
_build_normal_step
Keyword(name=data.name,
args=tuple(data.args),
assign=tuple(data.assign),
type=kw_type)
_build_templated_step
_format_template
for before, variable, after in iterator:
temp.extend([before, args.pop(0)])
temp.append(after)
robot.reporting.ResultWriter
from .model import ForLoop, Keyword, ResourceFile, TestSuite
ModelObject:
copy
deepcopy
__setstate__
ItemList
Keyword(ModelObject):
__slots__ = ['_name', 'doc', 'args', 'assign', 'timeout', 'type',
'_sort_key', '_next_child_sort_key']
KEYWORD_TYPE = 'kw' #: Normal keyword :attr:`type`.
SETUP_TYPE = 'setup' #: Setup :attr:`type`.
TEARDOWN_TYPE = 'teardown' #: Teardown :attr:`type`.
FOR_LOOP_TYPE = 'for' #: For loop :attr:`type`.
FOR_ITEM_TYPE = 'foritem' #: Single for loop iteration :attr:`type`.
keyword_class = None #: Internal usage only.
message_class = Message #: Internal usage only.
def __init__(self, name='', doc='', args=(), assign=(), tags=(),
timeout=None, type=KEYWORD_TYPE):
self.parent = None
self._name = name
self.doc = doc
self.args = args #: Keyword arguments as a list of strings.
self.assign = assign #: Assigned variables as a list of strings.
self.tags = tags
self.timeout = timeout
#: Keyword type as a string. The value is either :attr:`KEYWORD_TYPE`,
#: :attr:`SETUP_TYPE`, :attr:`TEARDOWN_TYPE`, :attr:`FOR_LOOP_TYPE` or
#: :attr:`FOR_ITEM_TYPE` constant defined on the class level.
self.type = type
self.messages = None
self.keywords = None
self._sort_key = -1
self._next_child_sort_key = 0
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
@setter
def parent(self, parent):
"""Parent test suite, test case or keyword."""
if parent and parent is not self.parent:
self._sort_key = getattr(parent, '_child_sort_key', -1)
return parent
@property
def _child_sort_key(self):
self._next_child_sort_key += 1
return self._next_child_sort_key
@setter
def tags(self, tags):
"""Keyword tags as a :class:`~.model.tags.Tags` object."""
return Tags(tags)
@setter
def keywords(self, keywords):
"""Child keywords as a :class:`~.Keywords` object."""
return Keywords(self.keyword_class or self.__class__, self, keywords)
@setter
def messages(self, messages):
"""Messages as a :class:`~.model.message.Messages` object."""
return Messages(self.message_class, self, messages)
@property
def children(self):
"""Child :attr:`keywords` and :attr:`messages` in creation order."""
# It would be cleaner to store keywords/messages in same `children`
# list and turn `keywords` and `messages` to properties that pick items
# from it. That would require bigger changes to the model, though.
return sorted(chain(self.keywords, self.messages),
key=attrgetter('_sort_key'))
@property
def id(self):
"""Keyword id in format like ``s1-t3-k1``.
See :attr:`TestSuite.id <robot.model.testsuite.TestSuite.id>` for
more information.
"""
if not self.parent:
return 'k1'
return '%s-k%d' % (self.parent.id, self.parent.keywords.index(self)+1)
def visit(self, visitor):
""":mod:`Visitor interface <robot.model.visitor>` entry-point."""
visitor.visit_keyword(self)
Keywords(ItemList)
__slots__ = []
def __init__(self, keyword_class=Keyword, parent=None, keywords=None):
ItemList.__init__(self, keyword_class, {'parent': parent}, keywords)
@property
def setup(self):
"""Keyword used as the setup or ``None`` if no setup.
Can be set to a new setup keyword or ``None`` since RF 3.0.1.
"""
return self[0] if (self and self[0].type == 'setup') else None
@setup.setter
def setup(self, kw):
if kw is not None and kw.type != 'setup':
raise TypeError("Setup keyword type must be 'setup', "
"got '%s'." % kw.type)
if self.setup is not None:
self.pop(0)
if kw is not None:
self.insert(0, kw)
@property
def teardown(self):
"""Keyword used as the teardown or ``None`` if no teardown.
Can be set to a new teardown keyword or ``None`` since RF 3.0.1.
"""
return self[-1] if (self and self[-1].type == 'teardown') else None
@teardown.setter
def teardown(self, kw):
if kw is not None and kw.type != 'teardown':
raise TypeError("Teardown keyword type must be 'teardown', "
"got '%s'." % kw.type)
if self.teardown is not None:
self.pop()
if kw is not None:
self.append(kw)
@property
def all(self):
"""Iterates over all keywords, including setup and teardown."""
return self
@property
def normal(self):
"""Iterates over normal keywords, omitting setup and teardown."""
kws = [kw for kw in self if kw.type not in ('setup', 'teardown')]
return Keywords(self._item_class, self._common_attrs['parent'], kws)
def __setitem__(self, index, item):
old = self[index]
ItemList.__setitem__(self, index, item)
self[index]._sort_key = old._sort_key
TestCase:
def tags(self, tags):
"""Test tags as a :class:`~.model.tags.Tags` object."""
return Tags(tags)
@setter
def keywords(self, keywords):
"""Keywords as a :class:`~.Keywords` object.
Contains also possible setup and teardown keywords.
"""
return Keywords(self.keyword_class, self, keywords)
class TestCases(ItemList):
__slots__ = []
def __init__(self, test_class=TestCase, parent=None, tests=None):
ItemList.__init__(self, test_class, {'parent': parent}, tests)
def _check_type_and_set_attrs(self, *tests):
tests = ItemList._check_type_and_set_attrs(self, *tests)
for test in tests:
for visitor in test.parent._visitors:
test.visit(visitor)
return tests
TestSuite:
def suites(self, suites):
"""Child suites as a :class:`~.TestSuites` object."""
return TestSuites(self.__class__, self, suites)
@setter
def tests(self, tests):
"""Tests as a :class:`~.TestCases` object."""
return TestCases(self.test_class, self, tests)
@setter
def keywords(self, keywords):
"""Suite setup and teardown as a :class:`~.Keywords` object."""
return Keywords(self.keyword_class, self, keywords)
TestSuites(ItemList):
__slots__ = []
def __init__(self, suite_class=TestSuite, parent=None, suites=None):
ItemList.__init__(self, suite_class, {'parent': parent}, suites)
model.py
class Keyword(model.Keyword):
"""Represents a single executable keyword.
These keywords never have child keywords or messages. The actual keyword
that is executed depends on the context where this model is executed.
See the base class for documentation of attributes not documented here.
"""
__slots__ = []
message_class = None #: Internal usage only.
def run(self, context):
"""Execute the keyword.
Typically called internally by :meth:`TestSuite.run`.
"""
return StepRunner(context).run_step(self)
class ForLoop(Keyword):
"""Represents a for loop in test data.
Contains keywords in the loop body as child :attr:`keywords`.
"""
__slots__ = ['flavor']
keyword_class = Keyword #: Internal usage only.
def __init__(self, variables, values, flavor):
Keyword.__init__(self, assign=variables, args=values,
type=Keyword.FOR_LOOP_TYPE)
self.flavor = flavor
@property
def variables(self):
return self.assign
@property
def values(self):
return self.args
class TestCase(model.TestCase):
"""Represents a single executable test case.
See the base class for documentation of attributes not documented here.
"""
__slots__ = ['template']
keyword_class = Keyword #: Internal usage only.
def __init__(self, name='', doc='', tags=None, timeout=None, template=None):
model.TestCase.__init__(self, name, doc, tags, timeout)
#: Name of the keyword that has been used as template
#: when building the test. ``None`` if no is template used.
self.template = template
@setter
def timeout(self, timeout):
"""Test timeout as a :class:`Timeout` instance or ``None``.
This attribute is likely to change in the future.
"""
return Timeout(*timeout) if timeout else None
class TestSuite(model.TestSuite):
"""Represents a single executable test suite.
See the base class for documentation of attributes not documented here.
"""
__slots__ = ['resource']
test_class = TestCase #: Internal usage only.
keyword_class = Keyword #: Internal usage only.
def __init__(self, name='', doc='', metadata=None, source=None, rpa=False):
model.TestSuite.__init__(self, name, doc, metadata, source, rpa)
#: :class:`ResourceFile` instance containing imports, variables and
#: keywords the suite owns. When data is parsed from the file system,
#: this data comes from the same test case file that creates the suite.
self.resource = ResourceFile(source=source)
def configure(self, randomize_suites=False, randomize_tests=False,
randomize_seed=None, **options):
"""A shortcut to configure a suite using one method call.
Can only be used with the root test suite.
:param randomize_xxx: Passed to :meth:`randomize`.
:param options: Passed to
:class:`~robot.model.configurer.SuiteConfigurer` that will then
set suite attributes, call :meth:`filter`, etc. as needed.
Example::
suite.configure(included_tags=['smoke'],
doc='Smoke test results.')
"""
model.TestSuite.configure(self, **options)
self.randomize(randomize_suites, randomize_tests, randomize_seed)
def randomize(self, suites=True, tests=True, seed=None):
"""Randomizes the order of suites and/or tests, recursively.
:param suites: Boolean controlling should suites be randomized.
:param tests: Boolean controlling should tests be randomized.
:param seed: Random seed. Can be given if previous random order needs
to be re-created. Seed value is always shown in logs and reports.
"""
self.visit(Randomizer(suites, tests, seed))
def run(self, settings=None, **options):
"""Executes the suite based based the given ``settings`` or ``options``.
:param settings: :class:`~robot.conf.settings.RobotSettings` object
to configure test execution.
:param options: Used to construct new
:class:`~robot.conf.settings.RobotSettings` object if ``settings``
are not given.
:return: :class:`~robot.result.executionresult.Result` object with
information about executed suites and tests.
If ``options`` are used, their names are the same as long command line
options except without hyphens. Some options are ignored (see below),
but otherwise they have the same semantics as on the command line.
Options that can be given on the command line multiple times can be
passed as lists like ``variable=['VAR1:value1', 'VAR2:value2']``.
If such an option is used only once, it can be given also as a single
string like ``variable='VAR:value'``.
Additionally listener option allows passing object directly instead of
listener name, e.g. ``run('tests.robot', listener=Listener())``.
To capture stdout and/or stderr streams, pass open file objects in as
special keyword arguments ``stdout`` and ``stderr``, respectively.
Only options related to the actual test execution have an effect.
For example, options related to selecting or modifying test cases or
suites (e.g. ``--include``, ``--name``, ``--prerunmodifier``) or
creating logs and reports are silently ignored. The output XML
generated as part of the execution can be configured, though. This
includes disabling it with ``output=None``.
Example::
stdout = StringIO()
result = suite.run(variable='EXAMPLE:value',
critical='regression',
output='example.xml',
exitonfailure=True,
stdout=stdout)
print result.return_code
To save memory, the returned
:class:`~robot.result.executionresult.Result` object does not
have any information about the executed keywords. If that information
is needed, the created output XML file needs to be read using the
:class:`~robot.result.resultbuilder.ExecutionResult` factory method.
See the :mod:`package level <robot.running>` documentation for
more examples, including how to construct executable test suites and
how to create logs and reports based on the execution results.
See the :func:`robot.run <robot.run.run>` function for a higher-level
API for executing tests in files or directories.
"""
from .namespace import IMPORTER
from .signalhandler import STOP_SIGNAL_MONITOR
from .runner import Runner
with LOGGER:
if not settings:
settings = RobotSettings(options)
LOGGER.register_console_logger(**settings.console_output_config)
with pyloggingconf.robot_handler_enabled(settings.log_level):
with STOP_SIGNAL_MONITOR:
IMPORTER.reset()
output = Output(settings)
runner = Runner(output, settings)
****
self.visit(runner)
output.close(runner.result)
return runner.result
class Variable(object):
def __init__(self, name, value, source=None):
self.name = name
self.value = value
self.source = source
def report_invalid_syntax(self, message, level='ERROR'):
LOGGER.write("Error in file '%s': Setting variable '%s' failed: %s"
% (self.source or '<unknown>', self.name, message), level)
class Timeout(object):
def __init__(self, value, message=None):
self.value = value
self.message = message
def __str__(self):
return self.value
class ResourceFile(object):
def __init__(self, doc='', source=None):
self.doc = doc
self.source = source
self.imports = []
self.keywords = []
self.variables = []
@setter
def imports(self, imports):
return model.Imports(self.source, imports)
@setter
def keywords(self, keywords):
return model.ItemList(UserKeyword, items=keywords)
@setter
def variables(self, variables):
return model.ItemList(Variable, {'source': self.source}, items=variables)
class UserKeyword(object):
def __init__(self, name, args=(), doc='', tags=(), return_=None, timeout=None):
self.name = name
self.args = args
self.doc = doc
self.tags = tags
self.return_ = return_ or ()
self.timeout = timeout
self.keywords = []
@setter
def keywords(self, keywords):
return model.Keywords(Keyword, self, keywords)
@setter
def timeout(self, timeout):
"""Keyword timeout as a :class:`Timeout` instance or ``None``."""
return Timeout(*timeout) if timeout else None
@setter
def tags(self, tags):
return model.Tags(tags)
StepRunner
def __init__(self, context, templated=False):
self._context = context
self._templated = bool(templated)
def run_steps(self, steps):
errors = []
for step in steps:
try:
self.run_step(step)
except ExecutionPassed as exception:
exception.set_earlier_failures(errors)
raise exception
except ExecutionFailed as exception:
errors.extend(exception.get_errors())
if not exception.can_continue(self._context.in_teardown,
self._templated,
self._context.dry_run):
break
if errors:
raise ExecutionFailures(errors)
def run_step(self, step, name=None):
context = self._context
if step.type == step.FOR_LOOP_TYPE:
runner = ForRunner(context, self._templated, step.flavor)
return runner.run(step)
runner = context.get_runner(name or step.name)
if context.dry_run:
return runner.dry_run(step, context)
return runner.run(step, context)
Runner:
def _run_setup_or_teardown(self, data):
if not data:
return None
try:
name = self._variables.replace_string(data.name)
except DataError as err:
if self._settings.dry_run:
return None
return err
if name.upper() in ('', 'NONE'):
return None
try:
StepRunner(self._context).run_step(data, name=name)
except ExecutionStatus as err:
return err
from .steprunner import StepRunner