chromium/content/test/gpu/validate_tag_consistency.py

#!/usr/bin/env vpython3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to ensure that the same tags are in all expectation files."""

from __future__ import print_function

import argparse
import logging
import os
import textwrap
from typing import Dict, List
import sys

# Constants for tag set generation.
LINE_START = '# '
TAG_SET_START = 'tags: [ '
TAG_SET_END = ' ]'
DEFAULT_LINE_LENGTH = 80 - len(LINE_START) - len(TAG_SET_START)
BREAK_INDENTATION = ' ' * 4

# Certain tags are technically subsets of other tags, e.g. win10 falls under the
# general win umbrella. We take this into account when checking for conflicting
# expectations, so we store the source of truth here and generate the resulting
# strings for the tag header.
TAG_SPECIALIZATIONS = {
    'OS_TAGS': {
        'android': [
            'android-oreo',
            'android-pie',
            'android-r',
            'android-s',
            'android-t',
            'android-14',
        ],
        'chromeos': [],
        'fuchsia': [],
        'linux': [
            'ubuntu',
        ],
        'mac': [
            'highsierra',
            'mojave',
            'catalina',
            'bigsur',
            'monterey',
            'ventura',
            'sonoma',
        ],
        'win': [
            'win8',
            'win10',
            'win11',
        ],
    },
    'BROWSER_TAGS': {
        'android-chromium': [],
        'android-webview-instrumentation': [],
        'debug': [
            'debug-x64',
        ],
        'release': [
            'release-x64',
        ],
        # These two are both Fuchsia-related.
        'fuchsia-chrome': [],
        'web-engine-shell': [],
        # These two are both ChromeOS-related.
        'lacros-chrome': [],
        'cros-chrome': [],
    },
    'GPU_TAGS': {
        'amd': [
            'amd-0x6613',
            'amd-0x679e',
            'amd-0x67ef',
            'amd-0x6821',
            'amd-0x7340',
        ],
        'apple': [
            'apple-apple-m1',
            'apple-apple-m2',
            'apple-angle-metal-renderer:-apple-m1',
            'apple-angle-metal-renderer:-apple-m2',
        ],
        'arm': [],
        'google': [
            'google-0xffff',
            'google-0xc0de',
        ],
        'imagination': [],
        'intel': [
            # Individual GPUs should technically fit under intel-gen-X, but we
            # only support one level of nesting, so treat the generation tags as
            # individual GPUs.
            'intel-gen-9',
            'intel-gen-12',
            'intel-0xa2e',
            'intel-0xd26',
            'intel-0xa011',
            'intel-0x3e92',
            'intel-0x3e9b',
            'intel-0x4680',
            'intel-0x5912',
            'intel-0x9bc5',
        ],
        'nvidia': [
            'nvidia-0xfe9',
            'nvidia-0x1cb3',
            'nvidia-0x2184',
            'nvidia-0x2783',
        ],
        'qualcomm': [
            # 043a = 0x41333430 = older Adreno GPU
            # 0636 = 0x36333630 = Adreno 690 GPU (such as Surface Pro 9 5G)
            # 0c36 = 0x36334330 = Adreno 741 GPU
            'qualcomm-0x41333430',
            'qualcomm-0x36333630',
            'qualcomm-0x36334330',
        ],
    },
}


def _GenerateTagSpecializationStrings() -> Dict[str, str]:
  """Generates string a string representation of |TAG_SPECIALIZATIONS|.

  The resulting dictionary can be fed directly into string.format().

  Returns:
    A dict mapping tag_set_name to tag_set_string. |tag_set_string| is the
    formatted, expectation parser-compatible string for the information
    contained within TAG_SPECIALIZATIONS[tag_set_name].
  """
  tag_specialization_strings = {}
  for tag_set_name, tag_set in TAG_SPECIALIZATIONS.items():
    # Create an appropriately wrapped set of lines for each group, join them,
    # and add the necessary bits to make them a parseable tag set.
    wrapped_tag_lines = []
    num_groups = len(tag_set)
    current_group = 0
    for general_tag, specialized_tags in tag_set.items():
      current_group += 1
      wrapped_tag_lines.extend(
          _CreateWrappedLinesForTagGroup([general_tag] + specialized_tags,
                                         current_group == num_groups))

    wrapped_tags_string = '\n'.join(wrapped_tag_lines)
    tag_set_string = ''
    for i, line in enumerate(wrapped_tags_string.splitlines(True)):
      tag_set_string += LINE_START
      if i == 0:
        tag_set_string += TAG_SET_START
      else:
        tag_set_string += (' ' * len(TAG_SET_START))
      tag_set_string += line
    tag_set_string += TAG_SET_END
    tag_specialization_strings[tag_set_name] = tag_set_string
  return tag_specialization_strings


def _CreateWrappedLinesForTagGroup(tag_group: List[str],
                                   is_last_group: bool) -> List[str]:
  tag_line = ' '.join(tag_group)
  line_length = DEFAULT_LINE_LENGTH
  # If this will be the last group, we have to make sure we wrap such that
  # there will be enough room for the closing bracket of the tag set.
  if is_last_group:
    line_length -= len(TAG_SET_END)
  return textwrap.wrap(tag_line,
                       width=line_length,
                       subsequent_indent=BREAK_INDENTATION,
                       break_on_hyphens=False)


TAG_HEADER = """\
# OS
{OS_TAGS}
# Devices
# tags: [ android-nexus-5x android-pixel-2 android-pixel-4
#             android-pixel-6 android-shield-android-tv android-sm-a135m
#             android-sm-a235m android-sm-s911u1 android-moto-g-power-5g---2023
#         chromeos-board-amd64-generic chromeos-board-eve chromeos-board-jacuzzi
#             chromeos-board-octopus chromeos-board-volteer
#         fuchsia-board-astro fuchsia-board-nelson fuchsia-board-sherlock
#             fuchsia-board-qemu-x64 ]
# Platform
# tags: [ desktop
#         mobile ]
# Browser
{BROWSER_TAGS}
# GPU
{GPU_TAGS}
# Architecture
# tags: [ mac-arm64 mac-x86_64 ]
# Decoder
# tags: [ passthrough no-passthrough ]
# Browser Target CPU
# tags: [ target-cpu-64 target-cpu-32 target-cpu-31 ]
# ANGLE Backend
# tags: [ angle-disabled
#         angle-d3d9 angle-d3d11
#         angle-metal
#         angle-opengl angle-opengles
#         angle-swiftshader
#         angle-vulkan ]
# Skia Renderer
# tags: [ renderer-skia-gl
#         renderer-skia-vulkan
#         renderer-software ]
# Driver
# tags: [ mesa_lt_19.1
#         mesa_ge_21.0
#         mesa_ge_23.2
#         nvidia_ge_31.0.15.4601 nvidia_lt_31.0.15.4601
#         nvidia_ge_535.183.01 nvidia_lt_535.183.01 ]
# ASan
# tags: [ asan no-asan ]
# Display Server
# tags: [ display-server-wayland display-server-x ]
# OOP-Canvas
# tags: [ oop-c no-oop-c ]
# WebGPU Backend Validation
# tags: [ dawn-backend-validation dawn-no-backend-validation ]
# WebGPU Adapter
# tags: [ webgpu-adapter-default webgpu-adapter-swiftshader ]
# WebGPU DXC
# tags: [ webgpu-dxc-enabled webgpu-dxc-disabled ]
# WebGPU worker usage
# tags: [ webgpu-no-worker
#         webgpu-service-worker
#         webgpu-dedicated-worker
#         webgpu-shared-worker ]
# Clang coverage
# tags: [ clang-coverage no-clang-coverage ]
# Skia Graphite
# tags: [ graphite-enabled graphite-disabled ]
# results: [ Failure RetryOnFailure Skip Slow ]
""".format(**_GenerateTagSpecializationStrings())

TAG_HEADER_BEGIN =\
    '# BEGIN TAG HEADER (autogenerated, see validate_tag_consistency.py)'
TAG_HEADER_END = '# END TAG HEADER'

EXPECTATION_DIR = os.path.join(os.path.dirname(__file__), 'gpu_tests',
                               'test_expectations')


def Validate():
  retval = 0
  for f in (f for f in os.listdir(EXPECTATION_DIR) if f.endswith('.txt')):
    with open(os.path.join(EXPECTATION_DIR, f)) as infile:
      content = infile.read()
      start_index = content.find(TAG_HEADER_BEGIN)
      end_index = content.find(TAG_HEADER_END)
      if (start_index < 0 or end_index < 0
          or content[start_index + len(TAG_HEADER_BEGIN) + 1:end_index] !=
          TAG_HEADER):
        retval = 1
        logging.error(
            'Expectation file %s does not have a tag/result header consistent '
            'with the source of truth.', f)
  if retval:
    logging.error(
        'See %s for the expected header or run it in the "apply" mode to apply '
        'the source of truth to all expectation files.', __file__)
  return retval


def Apply():
  retval = 0
  for f in (f for f in os.listdir(EXPECTATION_DIR) if f.endswith('.txt')):
    filepath = os.path.join(EXPECTATION_DIR, f)
    with open(filepath) as infile:
      content = infile.read()
    start_index = content.find(TAG_HEADER_BEGIN)
    if start_index < 0:
      retval = 1
      logging.error(
          'Expectation file %s did not have tag header start string "%s".', f,
          TAG_HEADER_BEGIN)
      continue
    end_index = content.find(TAG_HEADER_END)
    if end_index < 0:
      retval = 1
      logging.error(
          'Expectation file %s did not have tag header end string "%s".', f,
          TAG_HEADER_END)
      continue
    content = (content[:start_index + len(TAG_HEADER_BEGIN)] + '\n' +
               TAG_HEADER + content[end_index:])
    with open(filepath, 'w') as outfile:
      outfile.write(content)
  return retval


def main():
  parser = argparse.ArgumentParser(
      description=('Validate that all test expectation tags are identical '
                   'across all expectation files or apply the source of truth '
                   'to all expectation files.'))
  parser.add_argument('function',
                      choices=['apply', 'validate'],
                      help='What the script should do.')
  args = parser.parse_args()
  if args.function == 'apply':
    return Apply()
  return Validate()


if __name__ == '__main__':
  sys.exit(main())