Source code for robot.result.resultbuilder

#  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 = []