# 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.
from robot.errors import DataError
from robot.model import SuiteVisitor
from robot.utils import ET, ETSource, get_error_message
from .executionresult import Result, CombinedResult
from .flattenkeywordmatcher import (FlattenByNameMatcher, FlattenByTypeMatcher,
FlattenByTagMatcher)
from .merger import Merger
from .xmlelementhandlers import XmlElementHandler
[docs]def ExecutionResult(*sources, **options):
"""Factory method to constructs :class:`~.executionresult.Result` objects.
:param sources: XML source(s) containing execution results.
Can be specified as paths, opened file objects, or strings/bytes
containing XML directly. Support for bytes is new in RF 3.2.
:param options: Configuration options.
Using ``merge=True`` causes multiple results to be combined so that
tests in the latter results replace the ones in the original.
Setting ``rpa`` either to ``True`` (RPA mode) or ``False`` (test
automation) sets execution mode explicitly. By default it is got
from processed output files and conflicting modes cause an error.
Other options are passed directly to the
:class:`ExecutionResultBuilder` object used internally.
:returns: :class:`~.executionresult.Result` instance.
Should be imported by external code via the :mod:`robot.api` package.
See the :mod:`robot.result` package for a usage example.
"""
if not sources:
raise DataError('One or more data source needed.')
if options.pop('merge', False):
return _merge_results(sources[0], sources[1:], options)
if len(sources) > 1:
return _combine_results(sources, options)
return _single_result(sources[0], options)
def _merge_results(original, merged, options):
result = ExecutionResult(original, **options)
merger = Merger(result, rpa=result.rpa)
for path in merged:
merged = ExecutionResult(path, **options)
merger.merge(merged)
return result
def _combine_results(sources, options):
return CombinedResult(ExecutionResult(src, **options) for src in sources)
def _single_result(source, options):
ets = ETSource(source)
result = Result(source, rpa=options.pop('rpa', None))
try:
return ExecutionResultBuilder(ets, **options).build(result)
except IOError as err:
error = err.strerror
except:
error = get_error_message()
raise DataError(f"Reading XML source '{ets}' failed: {error}")
[docs]class ExecutionResultBuilder:
"""Builds :class:`~.executionresult.Result` objects based on output files.
Instead of using this builder directly, it is recommended to use the
:func:`ExecutionResult` factory method.
"""
def __init__(self, source, include_keywords=True, flattened_keywords=None):
"""
:param source: Path to the XML output file to build
:class:`~.executionresult.Result` objects from.
:param include_keywords: Controls whether to include keywords and control
structures like FOR and IF in the result or not. They are not needed
when generating only a report.
:param flattened_keywords: List of patterns controlling what keywords
and control structures to flatten. See the documentation of
the ``--flattenkeywords`` option for more details.
"""
self._source = source \
if isinstance(source, ETSource) else ETSource(source)
self._include_keywords = include_keywords
self._flattened_keywords = flattened_keywords
[docs] def build(self, result):
# Parsing is performance optimized. Do not change without profiling!
handler = XmlElementHandler(result)
with self._source as source:
self._parse(source, handler.start, handler.end)
result.handle_suite_teardown_failures()
if not self._include_keywords:
result.suite.visit(RemoveKeywords())
return result
def _parse(self, source, start, end):
context = ET.iterparse(source, events=('start', 'end'))
if not self._include_keywords:
context = self._omit_keywords(context)
elif self._flattened_keywords:
context = self._flatten_keywords(context, self._flattened_keywords)
for event, elem in context:
if event == 'start':
start(elem)
else:
end(elem)
elem.clear()
def _omit_keywords(self, context):
omitted_kws = 0
for event, elem in context:
# Teardowns aren't omitted yet to allow checking suite teardown status.
# They'll be removed later when not needed in `build()`.
omit = elem.tag in ('kw', 'for', 'if') and elem.get('type') != 'TEARDOWN'
start = event == 'start'
if omit and start:
omitted_kws += 1
if not omitted_kws:
yield event, elem
elif not start:
elem.clear()
if omit and not start:
omitted_kws -= 1
def _flatten_keywords(self, context, flattened):
# Performance optimized. Do not change without profiling!
name_match, by_name = self._get_matcher(FlattenByNameMatcher, flattened)
type_match, by_type = self._get_matcher(FlattenByTypeMatcher, flattened)
tags_match, by_tags = self._get_matcher(FlattenByTagMatcher, flattened)
started = -1 # if 0 or more, we are flattening
tags = []
containers = {'kw', 'for', 'while', 'iter', 'if', 'try'}
inside_kw = 0 # to make sure we don't read tags from a test
seen_doc = False
for event, elem in context:
tag = elem.tag
start = event == 'start'
end = not start
if start:
if tag in containers:
inside_kw += 1
if started >= 0:
started += 1
elif by_name and name_match(elem.get('name', ''), elem.get('library')):
started = 0
seen_doc = False
elif by_type and type_match(tag):
started = 0
seen_doc = False
tags = []
else:
if tag in containers:
inside_kw -= 1
if started == 0 and not seen_doc:
doc = ET.Element('doc')
doc.text = '_*Content flattened.*_'
yield 'start', doc
yield 'end', doc
elif by_tags and inside_kw and started < 0 and tag == 'tag':
tags.append(elem.text or '')
if tags_match(tags):
started = 0
seen_doc = False
elif started == 0 and tag == 'doc':
seen_doc = True
elem.text = f"{elem.text or ''}\n\n_*Content flattened.*_".strip()
if started <= 0 or tag == 'msg':
yield event, elem
else:
elem.clear()
if started >= 0 and end and tag in containers:
started -= 1
def _get_matcher(self, matcher_class, flattened):
matcher = matcher_class(flattened)
return matcher.match, bool(matcher)
[docs]class RemoveKeywords(SuiteVisitor):
[docs] def start_suite(self, suite):
suite.setup = None
suite.teardown = None
[docs] def visit_test(self, test):
test.body = []