Source code for testplan.testing.multitest.entries.stdout.assertions

"""Loggers for assertion objects"""
import os
import pprint
import re
from itertools import chain

from terminaltables import AsciiTable

import testplan.common.exporters.constants as constants
from testplan.common.exporters.pdf import format_cell_data
from testplan.common.utils.strings import Color, map_to_str

from .. import assertions
from .base import BaseRenderer, registry


[docs]@registry.bind_default(category="assertion") class AssertionRenderer(BaseRenderer): """ Default assertion logger. Renders simple details (file & line no), and assertion name/description and pass/fail status as header. """
[docs] def pass_label(self, entry): return Color.green("Pass") if entry else Color.red("Fail")
[docs] def get_assertion_details(self, entry): return None
[docs] def get_details(self, entry): """ Return file & line no (failing entries only), along with the extra info returned by `get_assertion_details`. """ # pylint: disable=assignment-from-none assertion_details = self.get_assertion_details(entry) # pylint: enable=assignment-from-none if not entry: details = "File: {}".format(entry.file_path) details += os.linesep + "Line: {}".format(entry.line_no) if assertion_details: details += os.linesep + assertion_details return details return assertion_details
[docs] def get_header(self, entry): return "{} - {}".format( self.get_header_text(entry), self.pass_label(entry) )
[docs]@registry.bind( assertions.Equal, assertions.NotEqual, assertions.Less, assertions.LessEqual, assertions.Greater, assertions.GreaterEqual, ) class FunctionAssertionRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """ Use a format like `1 == 2`, highlighting failing comparisons in red. """ msg = "{} {} {}".format(entry.first, entry.label, entry.second) return msg if entry else Color.red(msg)
[docs]@registry.bind(assertions.IsClose) class ApproximateEqualityAssertionRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """ Use a format like `99 ~= 100 (with rel_tol=0.1, abs_tol=0.0)`, highlighting failing comparisons in red. """ msg = "{} {} {} (rel_tol: {}, abs_tol: {})".format( entry.first, entry.label, entry.second, entry.rel_tol, entry.abs_tol, ) return msg if entry else Color.red(msg)
[docs]@registry.bind(assertions.RegexMatch, assertions.RegexSearch) class RegexMatchRenderer(AssertionRenderer): highlight_color = "green"
[docs] def get_assertion_details(self, entry): """ Return highlighted patterns within the string, if there is a match. Note that pattern & string (despite the name) could be bytes """ string = entry.string pattern = "Pattern: `{}`{}".format(entry.pattern, os.linesep) if entry.match_indexes: curr_idx = 0 parts = [] for begin, end in entry.match_indexes: if begin > curr_idx: parts.append(map_to_str(string[curr_idx:begin])) parts.append( Color.colored(string[begin:end], self.highlight_color) ) curr_idx = end if curr_idx < len(string): parts.append(string[curr_idx:]) return "{}{}".format(pattern, "".join(parts)) else: return "{}{}".format(pattern, string)
[docs]@registry.bind(assertions.RegexFindIter) class RegexFindIterRenderer(RegexMatchRenderer):
[docs] def get_assertion_details(self, entry): """Render `condition` attribute as well, if it exists""" msg = super(RegexFindIterRenderer, self).get_assertion_details(entry) if entry.condition_match is not None: msg += "{}Condition: {}".format( os.linesep, Color.colored( entry.condition, color="green" if entry.condition_match else "red", ), ) return msg
[docs]@registry.bind(assertions.RegexMatchLine) class RegexMatchLineRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """ `RegexMatchLine` returns line indexes along with begin/end character indexes per matched line. Note: pattern & string (despite the name) could be bytes """ pattern = "Pattern: `{}`{}".format(entry.pattern, os.linesep) if entry.match_indexes: parts = [] match_map = { line_no: (begin, end) for line_no, begin, end in entry.match_indexes } for idx, line in enumerate(entry.lines): if idx in match_map: begin, end = match_map[idx] parts.append( line[:begin] + Color.green(line[begin:end]) + line[end:] ) else: parts.append(line) return "{}{}".format(pattern, os.linesep.join(parts)) return "{}{}".format(pattern, entry.string)
[docs]@registry.bind(assertions.RegexMatchNotExists, assertions.RegexSearchNotExists) class RegexNotMatchRenderer(RegexMatchRenderer): highlight_color = "red"
[docs]@registry.bind(assertions.Contain, assertions.NotContain) class MembershipRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """Return the member and container representations""" op_label = ( "not in" if isinstance(entry, assertions.NotContain) else "in" ) return "{} {} {}".format(entry.member, op_label, entry.container)
[docs]@registry.bind(assertions.TableMatch, assertions.TableDiff) class TableMatchRenderer(AssertionRenderer): """ Renders tabular data in ASCII table format Sample output: +----------+----------------+ | age | name | +----------+----------------+ | 32 == 32 | Bob == Bob | | 24 == 24 | Susan == Susan | | 67 == 67 | Rick != David | +----------+----------------+ """
[docs] def get_row_data( self, row_comparison, columns, include_columns=None, exclude_columns=None, display_index=False, ): """Return single row data to be printed""" def fmt(val): return val if val is not None else "" result = [] for idx, column in enumerate(columns): actual = row_comparison.data[idx] other, matched = row_comparison.get_comparison_value(column, idx) value_limit = int((constants.CELL_STRING_LENGTH - 4) / 2) other, actual = format_cell_data( data=[other, actual], limit=value_limit ) include_columns = include_columns or columns exclude_columns = exclude_columns or [] if ( (column not in include_columns) or (column in exclude_columns) or matched is None ): result.append(f"{fmt(actual)} .. {fmt(other)}") elif matched: result.append(Color.green(f"{fmt(actual)} == {fmt(other)}")) else: result.append(Color.red(f"{fmt(actual)} != {fmt(other)}")) if display_index: result = [row_comparison.idx] + result return result
[docs] def get_assertion_details(self, entry): """Return row by row match results in tabular format""" if entry.message: result = ( Color.red(entry.message) if not entry.passed else entry.message ) else: result = "" row_data = [ self.get_row_data( row_comparison, entry.display_columns, display_index=entry.report_fails_only, ) for row_comparison in entry.data ] columns = ( ["row"] + list(entry.display_columns) if entry.report_fails_only else entry.display_columns ) ascii_table = ( AsciiTable([columns] + row_data).table if row_data else "" ) return "{}{}{}".format( result, os.linesep if result and ascii_table else "", ascii_table )
[docs]@registry.bind(assertions.ColumnContain) class ColumnContainRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): ascii_columns = [entry.column, "Passed"] if entry.report_fails_only: ascii_columns = ["row"] + ascii_columns table = [] for comp_obj in entry.data: ascii_row = [comp_obj.value, Color.passed(check=comp_obj.passed)] if entry.report_fails_only: ascii_row = [comp_obj.idx] + ascii_row table.append(ascii_row) return "{}{}{}".format( "Values: {}".format(entry.values), os.linesep, AsciiTable([ascii_columns] + table).table, )
[docs]def add_printable_dict_comparison(result, row): """Stdout representation of fix/dict match rows.""" indent, key, status, left, right = row if left or right: if not left: left = "None" if not right: right = "None" else: left = "" right = "" if status == "Passed": coloured = Color.colored(status, "green") operator = " == " elif status == "Failed": coloured = Color.colored(status, "red") operator = " != " else: coloured = "Ignore" operator = " " result.append( "{} {}{} {}{}{}".format( "({})".format(coloured) if coloured else " " * len("(Passed)"), " " * 4 * indent, "Key({}),".format(key) if key else "", "{} <{}>".format(left[1], left[0]) if isinstance(left, tuple) else left, operator if left and right else " ", "{} <{}>".format(right[1], right[0]) if isinstance(right, tuple) else right, ) )
[docs]@registry.bind(assertions.DictMatch, assertions.FixMatch) class DictMatchRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """Return fix and dict match result representations""" result = [] for row in entry.comparison: add_printable_dict_comparison(result, row) if result: result.append("") return str(os.linesep.join(result))
[docs]@registry.bind(assertions.DictCheck, assertions.FixCheck) class DictCheckRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """Return `has_keys` & `absent_keys` context""" msg = "" if entry.has_keys: msg += "Existence check: {}".format(entry.has_keys) if entry.has_keys_diff: msg += "{}\tMissing keys: {}".format( os.linesep, Color.red(list(entry.has_keys_diff)) ) if entry.absent_keys: if msg: msg += os.linesep msg += "Absence check: {}".format(entry.absent_keys) if entry.absent_keys_diff: msg += "{}\tKey should be absent: {}".format( os.linesep, Color.red(list(entry.absent_keys_diff)) ) return msg
[docs]@registry.bind(assertions.DictMatchAll, assertions.FixMatchAll) class DictMatchAllRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): """Return fix and dict match_all result representations""" result = [] for match in entry.matches: comparison = match["comparison"] description = match["description"] passed = match["passed"] if passed is True: coloured = Color.colored("Passed", "green") else: coloured = Color.colored("Failed", "red") result.append("({}) {}".format(coloured, description)) for row in comparison: add_printable_dict_comparison(result, row) if result: result.append("") return str(os.linesep.join(result))
[docs]@registry.bind(assertions.ExceptionRaised, assertions.ExceptionNotRaised) class ExceptionRaisedRenderer(AssertionRenderer):
[docs] def get_assertion_details(self, entry): raised_exc = entry.raised_exception expected_exceptions = ", ".join( [exc.__name__ for exc in entry.expected_exceptions] ) label = ( "not instance of" if isinstance(entry, assertions.ExceptionNotRaised) else "instance of" ) msg = "{} {} {}".format(type(raised_exc), label, expected_exceptions) if entry.func: msg += "{} Function: {}".format( os.linesep, str(entry.func) if entry.passed else Color.red(str(entry.func)), ) if entry.pattern: msg += "{} Pattern: {}".format( os.linesep, entry.pattern if entry.passed else Color.red(entry.pattern), ) msg += "{} Exception message: {}".format( os.linesep, entry.raised_exception ) return msg
[docs]@registry.bind(assertions.XMLCheck) class XMLCheckRenderer(AssertionRenderer): """Renderer for XMLCheck"""
[docs] def get_assertion_details(self, entry): """ Render the message if there is any, then render XMLTagComparison items. """ result = [] result.append("xpath: {}".format(entry.xpath)) if entry.namespaces: result.append("Namespaces: {}".format(entry.namespaces)) if entry.message: result.append( entry.message if entry.passed else Color.red(entry.message) ) if entry.data: result.append("Tags:") for tag_comp in entry.data: template = " {actual} {operator} {expected}" common = dict( actual=tag_comp.tag, expected=tag_comp.comparison_value ) if tag_comp.passed: result.append(template.format(operator="==", **common)) else: result.append( Color.red(template.format(operator="!=", **common)) ) return os.linesep.join(result)
[docs]@registry.bind(assertions.Fail) class FailRenderer(AssertionRenderer):
[docs] def get_header(self, entry): return Color.red(entry.description)
[docs] def get_assertion_details(self, entry): if isinstance(entry.message, str): if entry.description: return Color.red(str(entry.message)) else: return None else: return pprint.pformat(entry.message)
[docs]@registry.bind(assertions.EqualSlices) class EqualSlicesRenderer(AssertionRenderer): """ Display slice, comparison indexes, mismatch indexes, actual and expected iterables. Sample output: Equal Slices - Pass slice(2, 4, None) Actual: [3, 4] Expected: [3, 4] slice(5, 7, None) Actual: ['d', 'e'] Expected: [6, 7] """
[docs] def get_assertion_details(self, entry): result = [] for slice_comp in entry.data: result.append( "{}".format( slice_comp.slice if slice_comp.passed else Color.red(slice_comp.slice) ) ) # Display mismatched indexes if slice has # a step, for easier debugging if slice_comp.mismatch_indices: result.append( " Mismatched indices: {}".format( slice_comp.mismatch_indices ) ) result.append(" Actual:\t{}".format(slice_comp.actual)) result.append(" Expected:\t{}".format(slice_comp.expected)) return os.linesep.join(result)
[docs]@registry.bind(assertions.EqualExcludeSlices) class EqualExcludeSlicesRenderer(AssertionRenderer): """ Display excluded indexes, compared indexes, actual and expected iterables. """
[docs] def get_assertion_details(self, entry): result = [ "{} - excluded".format(slice_comp.slice) for slice_comp in entry.data ] actual = [entry.actual[i] for i in entry.included_indices] expected = [entry.expected[i] for i in entry.included_indices] result.extend( [ "{}: {}".format( title, data if entry.passed else Color.red(data) ) for title, data in ( ("Compared indices", list(entry.included_indices)), ("Actual", actual), ("Expected", expected), ) ] ) return os.linesep.join(result)
[docs]@registry.bind(assertions.LineDiff) class LineDiffRenderer(AssertionRenderer): """ Display 2 blocks of textual content, truncate them if too long, also display the difference between them if found. """
[docs] def get_assertion_details(self, entry): result = [] if not entry.passed: result.append(Color.red("*** a.text ***" + os.linesep)) for line in self._get_truncated_lines( entry.first, n=constants.NUM_DISPLAYED_ROWS ): result.append(" {}".format(line)) if not (result[-1].endswith(os.linesep) or result[-1][-1] == "\n"): result[-1] += os.linesep result.append(Color.red("*** b.text ***" + os.linesep)) for line in self._get_truncated_lines( entry.second, n=constants.NUM_DISPLAYED_ROWS ): result.append(" {}".format(line)) if not (result[-1].endswith(os.linesep) or result[-1][-1] == "\n"): result[-1] += os.linesep options = "" options += "-b " if entry.ignore_space_change else "" options += "-w " if entry.ignore_whitespaces else "" options += "-B " if entry.ignore_blank_lines else "" options += "-u " if entry.unified else "" options += "-c " if entry.context and not entry.unified else "" options = " ( " + options + ")" if options else "" if entry.passed: result.append("No difference found{}".format(options)) else: result.append( Color.red("Differences{}:{}".format(options, os.linesep)) ) for line in self._get_truncated_lines( entry.delta, n=constants.NUM_DISPLAYED_ROWS * 5 ): result.append(" {}".format(line)) result[-1] = re.sub(r"[\r\n]+$", "", result[-1]) return "".join(result)
def _get_truncated_lines(self, lines, n=0): # At most keep n lines, n==0 means no truncation. for i, line in enumerate(lines): if n > 0 and i >= n: yield "[truncated after displaying first %d lines ...]" % n break yield line
[docs]@registry.bind(assertions.LogfileMatch) class LogfileMatchRender(AssertionRenderer):
[docs] def get_assertion_details(self, entry: assertions.LogfileMatch): return os.linesep.join( f"Pattern: `{e.pattern}`" if e.matched else Color.red(f"Pattern: `{e.pattern}`") for e in chain(entry.results, entry.failure) )