Source code for testplan.exporters.testing.pdf.renderers.entries.base

""" TODO """
import pprint

from PIL import Image as pil_image
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.platypus import Image

from testplan.common.exporters.pdf import RowStyle, create_table
from testplan.common.exporters.pdf import format_table_style
from testplan.common.utils.registry import Registry
from testplan.common.utils.strings import split_text
from testplan.exporters.testing.pdf.renderers.base import SlicedParagraph
from testplan.testing.multitest.entries import base
from .baseUtils import get_matlib_plot, export_plot_to_image, format_image
from .. import constants
from ..base import BaseRowRenderer, RowData


[docs]class SerializedEntryRegistry(Registry): """ Registry that is used for binding assertion classes to PDF renderers. Keep in mind that we pass around serialized version of assertion objects (generated via `multitest.entries.schemas`) meaning that lookup arguments will be dictionary representation instead of assertion object instances, hence the need to use class names instead of class objects for `data` keys. """
[docs] def get_record_key(self, obj): return obj.__name__
[docs] def get_lookup_key(self, obj): return obj["type"]
[docs] def get_category(self, obj): return obj["meta_type"]
registry = SerializedEntryRegistry()
[docs]@registry.bind_default() class SerializedEntryRenderer(BaseRowRenderer): """Default fallback for all serialized entries."""
[docs] def get_header(self, source, depth, row_idx): """Display the description or type as the header.""" styles = [ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * depth, ) ] header = split_text( source["description"] or source["type"], constants.FONT, constants.FONT_SIZE_SMALL, constants.PAGE_WIDTH - (depth * constants.INDENT), ) return RowData( content=[header, "", "", ""], style=styles, start=row_idx )
[docs] def get_row_content(self, source): """ All entries will either have a description or type, we display whatever is available. """ return [source["description"] or source["type"], "", "", ""]
[docs] def get_row_data(self, source, depth, row_idx): """ Most entries will be rendered as single rows, so we use `get_row_content` and `get_row_style` for simplicity. """ result = RowData( content=self.get_row_content(source), style=self.get_row_style(source, depth), start=row_idx, ) return result
[docs] def get_row_style(self, source, depth, **kwargs): """Default styling for all serialized entries, with small font size.""" return RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * depth, **kwargs )
[docs] def get_style(self, source): if "passed" in source and source["passed"] is False: return self.style.failing return self.style.passing
[docs] def should_display(self, source): return self.get_style(source).display_assertion
[docs]@registry.bind(base.Log) class LogRenderer(SerializedEntryRenderer):
[docs] def get_header(self, source, depth, row_idx): """ Display the description or the log message (if no description) as the header. """ styles = [ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * depth, span=(), ) ] # log assertion is guaranteed to have description field header = source["description"] return RowData( content=[header, "", "", ""], style=styles, start=row_idx )
[docs] def get_row_data(self, source, depth, row_idx): """ Create a header and a detailed log message, only create a head if should display a string message without the description. """ header = self.get_header(source, depth, row_idx) log_msg = ( str(source["message"]) if isinstance(source["message"], str) else pprint.pformat(source["message"], depth=6) ) left_padding = constants.INDENT * (depth + 1) for para in SlicedParagraph( parts=(log_msg, "{}"), width=constants.PAGE_WIDTH - left_padding - 6, ): header = header + RowData( content=[para, "", "", ""], style=[RowStyle(left_padding=left_padding, span=tuple())], start=header.end, ) return header
[docs]@registry.bind(base.Attachment) class AttachmentRenderer(SerializedEntryRenderer): """Render an assertion of attaching file from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): """Display path of attached file.""" header = self.get_header(source, depth, row_idx) row_idx += 1 return header + RowData( content=[source["source_path"], "", "", ""], style=[ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + 1), text_color=colors.black, ) ], start=row_idx, )
[docs]@registry.bind(base.MatPlot) class MatPlotRenderer(SerializedEntryRenderer): """Render a Matplotlib assertion from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): """ Load the Matplotlib graph from the saved image, set its height and width and add it to the row. """ header = self.get_header(source, depth, row_idx) styles = [ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + 1), text_color=colors.black, ) ] p_img = pil_image.open(source["source_path"]) dpi_w, dpi_h = p_img.info["dpi"] width = p_img.width / dpi_w * inch height = p_img.height / dpi_h * inch img = Image(source["source_path"], width, height) return header + RowData( content=[img, "", "", ""], start=header.end, style=styles, )
[docs]@registry.bind(base.Plotly) class PlotlyRenderer(SerializedEntryRenderer): """Render a Plotly assertion from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): # TODO Add PDF render # https://plotly.com/python/static-image-export/ # require kaleido which not support rhel6 return super(PlotlyRenderer, self).get_row_data(source, depth, row_idx)
# # header = self.get_header(source, depth, row_idx) # styles = [ # RowStyle( # font=(constants.FONT, constants.FONT_SIZE_SMALL), # left_padding=constants.INDENT * (depth + 1), # text_color=colors.black, # ) # ] # # fig = plotly.io.read_json(source["source_path"]) # # img_file = str(pathlib.Path(source["source_path"]).with_suffix(".jpg")) # plotly.io.write_image(fig, img_file) # p_img = pil_image.open(img_file) # dpi_w, dpi_h = p_img.info["dpi"] # # width = p_img.width / dpi_w * inch # height = p_img.height / dpi_h * inch # # img = Image(img_file, width, height) # # return header + RowData( # content=[img, "", "", ""], # start=header.end, # style=styles, # )
[docs]@registry.bind(base.Directory) class DirectoryRenderer(SerializedEntryRenderer): """Render an assertion of attaching directory from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): """Display path of attached directory.""" header = self.get_header(source, depth, row_idx) row_idx += 1 header += RowData( content=[source["source_path"], "", "", ""], style=[ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + 1), text_color=colors.black, ) ], start=row_idx, ) row_idx += 1 for idx, fpath in enumerate(source["file_list"]): header += RowData( content=[fpath, "", "", ""], style=[ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + 2), text_color=colors.black, ) ], start=row_idx + idx, ) return header
[docs]@registry.bind(base.TableLog) class TableLogRenderer(SerializedEntryRenderer): """Render a Table from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): """ Reformat the rows from the serialized data into a format ReportLab accepts. Create a header and a ReportLab table and add it to the row. """ header = self.get_header(source, depth, row_idx) row_style = [RowStyle(left_padding=constants.INDENT * (depth + 1))] table_style = format_table_style(constants.DISPLAYED_TABLE_STYLE) max_width = constants.PAGE_WIDTH - (depth * constants.INDENT) table = create_table( table=source["table"], columns=source["columns"], row_indices=source["indices"], display_index=source["display_index"], max_width=max_width, style=table_style, ) return header + RowData( content=table, start=header.end, style=row_style )
[docs]@registry.bind(base.DictLog, base.FixLog) class DictLogRenderer(SerializedEntryRenderer): """Render a mapping object from a serialized entry."""
[docs] def get_row_data(self, source, depth, row_idx): """ Reformat the rows from the serialized data into a format ReportLab accepts. Create a header and rows of flattened mapping object. """ header = self.get_header(source, depth, row_idx) row_idx += 1 if len(source["flattened_dict"]) == 0: return header + RowData( content=["(empty)", "", "", ""], style=[ RowStyle( text_color=colors.black, font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + 1), ) ], start=row_idx, ) for idx, row in enumerate(source["flattened_dict"]): offset, key, val = row header += RowData( content=[ key, "<{}> {}".format(val[0], val[1]) if isinstance(val, (tuple, list)) else "", "", "", ], style=[ RowStyle( text_color=colors.black, font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=constants.INDENT * (depth + offset + 1), ) ], start=row_idx + idx, ) return header
[docs]@registry.bind(base.Graph) class GraphRenderer(SerializedEntryRenderer):
[docs] def get_row_data(self, source, depth, row_idx): """ Load the graph as a static image using MatPlotLib """ header = self.get_header(source, depth, row_idx) styles = [ RowStyle( font=(constants.FONT, constants.FONT_SIZE_SMALL), left_padding=20, text_color=colors.black, ) ] graph_plot = get_matlib_plot(source) if graph_plot is None: image = "Unable to render " + source["graph_type"] return header + RowData( content=image, start=header.end, style=styles ) else: image = export_plot_to_image(graph_plot) image = format_image(image) # graph_plot must be cleared for next graph to render correctly graph_plot.clf() graph_plot.cla() graph_plot.close() return header + RowData( content=[image, "", "", ""], start=header.end, style=styles )