chromium/testing/flake_suppressor_common/result_output.py

# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module for outputting results in a human-readable format."""

import tempfile
from typing import Dict, IO, List, Optional, Union

from flake_suppressor_common import common_typing as ct

UrlListType = List[str]
StringTagsToUrlsType = Dict[str, UrlListType]
TestToStringTagsType = Dict[str, StringTagsToUrlsType]
StringMapType = Dict[str, TestToStringTagsType]

TestToUrlListType = Dict[str, UrlListType]
SuiteToTestsType = Dict[str, TestToUrlListType]
ConfigGroupedStringMapType = Dict[str, SuiteToTestsType]

NodeType = Union[UrlListType, StringTagsToUrlsType, TestToStringTagsType,
                 StringMapType, TestToUrlListType, SuiteToTestsType,
                 ConfigGroupedStringMapType]


def GenerateHtmlOutputFile(aggregated_results: ct.AggregatedResultsType,
                           outfile: Optional[IO] = None) -> None:
  """Generates an HTML results file.

  Args:
    aggregated_results: A map containing the aggregated test results.
    outfile: A file-like object to output to. Will create one if not provided.
  """
  outfile = outfile or tempfile.NamedTemporaryFile(
      mode='w', delete=False, suffix='.html')
  try:
    outfile.write('<html>\n<body>\n')
    string_map = _ConvertAggregatedResultsToStringMap(aggregated_results)
    _OutputMapToHtmlFile(string_map, 'Grouped By Test', outfile)
    config_map = _ConvertFromTestGroupingToConfigGrouping(string_map)
    _OutputMapToHtmlFile(config_map, 'Grouped By Config', outfile)
    outfile.write('</body>\n</html>\n')
  finally:
    outfile.close()
  print('HTML results: %s' % outfile.name)


def _OutputMapToHtmlFile(string_map: StringMapType, result_header: str,
                         output_file: IO) -> None:
  """Outputs a map to a file as a nested list.

  Args:
    string_map: The string map to output.
    result_header: A string containing the header contents placed before the
        nested list.
    output_file: A file-like object to output the map to.
  """
  output_file.write('<h1>%s</h1>\n' % result_header)
  output_file.write('<ul>\n')
  _RecursiveHtmlToFile(string_map, output_file)
  output_file.write('</ul>\n')


def _RecursiveHtmlToFile(node: NodeType, output_file: IO) -> None:
  """Recursively outputs a string map to an output file as HTML.

  Specifically, contents are output as an unordered list (<ul>).

  Args:
    node: The current node to output. Must be either a dict or list.
    output_file: A file-like object to output the HTML to.
  """
  if isinstance(node, dict):
    for key, value in node.items():
      output_file.write('<li>%s</li>\n' % key)
      output_file.write('<ul>\n')
      _RecursiveHtmlToFile(value, output_file)
      output_file.write('</ul>\n')
  elif isinstance(node, list):
    for element in node:
      output_file.write('<li><a href="%s">%s</a></li>\n' % (element, element))
  else:
    raise RuntimeError('Unsupported type %s' % type(node).__name__)


def _ConvertAggregatedResultsToStringMap(
    aggregated_results: ct.AggregatedResultsType) -> StringMapType:
  """Converts aggregated results to a format usable by _RecursiveHtmlToFile.

  Specifically, updates the string representation of the typ tags and replaces
  the lowest level dict with the build URL list.

  Args:
    aggregated_results: A map containing the aggregated test results.

  Returns:
    A map in the format:
    {
      'suite': {
        'test': {
          'space separated typ tags': ['build', 'url', 'list']
        }
      }
    }
  """
  string_map = {}
  for suite, test_map in aggregated_results.items():
    for test, tag_map in test_map.items():
      for typ_tags, build_url_list in tag_map.items():
        str_typ_tags = ' '.join(typ_tags)
        string_map.setdefault(suite,
                              {}).setdefault(test,
                                             {})[str_typ_tags] = build_url_list
  return string_map


def _ConvertFromTestGroupingToConfigGrouping(string_map: StringMapType
                                             ) -> ConfigGroupedStringMapType:
  """Converts |string| map to be grouped by typ tags/configuration.

  Args:
    string_map: The output of _ConvertAggregatedResultsToStringMap.

  Returns:
    A map in the format:
    {
      'space separated typ tags': {
        'suite': {
          'test': ['build', 'url', 'list']
        }
      }
    }
  """
  converted_map = {}
  for suite, test_map in string_map.items():
    for test, tag_map in test_map.items():
      for typ_tags, build_urls in tag_map.items():
        converted_map.setdefault(typ_tags, {}).setdefault(suite,
                                                          {})[test] = build_urls
  return converted_map