chromium/tools/metrics/histograms/generate_allowlist_from_histograms_file.py

#!/usr/bin/env python
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import os
import sys

import extract_histograms
import xml.dom.minidom

_SCRIPT_NAME = "generate_allowlist_from_histograms_file.py"
_FILE = """// Generated from {script_name}. Do not edit!

#ifndef {include_guard}
#define {include_guard}

#include <algorithm>
#include <string_view>

namespace {namespace} {{

inline constexpr std::string_view k{allow_list_name}AllowList[] = {{
{values}
}};

constexpr bool IsValid{allow_list_name}(std::string_view s) {{
  return std::binary_search(
    std::cbegin(k{allow_list_name}AllowList),
    std::cend(k{allow_list_name}AllowList),
    s);
}}

}}  // namespace {namespace}

#endif  // {include_guard}
"""


class Error(Exception):
  pass


def _GenerateStaticFile(file_path, namespace, values, allow_list_name):
  """Generates a header file with constexpr facilities to check for the
    existence of a variant or enum in a histograms.xml file.

  Args:
    namespace: A namespace to contain generated array.
    values(List[str|int]]): A list of variant or enum values.
    allow_list_name: A name of the variant list for an allow list.
  Returns:
    String with the generated header file content.
  """
  values = sorted(values, key=lambda d: str(d))
  include_guard = file_path.replace('\\', '_').replace('/', '_').replace(
      '.', '_').upper() + "_"

  values_string = "\n".join(
      ["  \"{name}\",".format(name=value) for value in values])
  return _FILE.format(script_name=_SCRIPT_NAME,
                      include_guard=include_guard,
                      namespace=namespace,
                      values=values_string,
                      allow_list_name=allow_list_name)


def _GenerateValueList(histograms, tag, allow_list_name):
  if tag == "variant":
    values, had_errors = extract_histograms.ExtractVariantsFromXmlTree(
        histograms)
  elif tag == "enum":
    values, had_errors = extract_histograms.ExtractEnumsFromXmlTree(histograms)
  else:
    raise Error("'tag' must be either 'variant' or 'enum'")

  if had_errors:
    raise Error("Error parsing inputs.")

  if (allow_list_name not in values):
    raise Error("AllowListName is missing in variants list")

  if tag == "variant":
    return [value["name"] for value in values[allow_list_name]]
  else:
    return list(values[allow_list_name]["values"].keys())


def _GenerateFile(arguments):
  """Generates C++ header file containing values of a variant or enum from
  a .xml file.

  Args:
    arguments: An object with the following attributes:
      arguments.input: An xml file with histogram descriptions.
      arguments.file: A filename of the generated source file.
      arguments.tag: A XML tag, can be "enum" or "variant".
      arguments.namespace: A namespace to contain generated array.
      arguments.output_dir: A directory to put the generated file.
      arguments.allow_list_name: A name of the variant or enum list.
  """
  histograms = xml.dom.minidom.parse(arguments.input)
  values = _GenerateValueList(histograms, arguments.tag,
                              arguments.allow_list_name)

  static_check_header_file_content = _GenerateStaticFile(
      arguments.file, arguments.namespace, values, arguments.allow_list_name)
  with open(os.path.join(arguments.output_dir, arguments.file),
            "w") as generated_file:
    generated_file.write(static_check_header_file_content)


def _ParseArguments():
  """Defines and parses arguments from the command line."""
  arg_parser = argparse.ArgumentParser(
      description="Generate an array of allowlist from a histograms.xml file."
  )
  arg_parser.add_argument("--output_dir",
                          required=True,
                          help="Base directory to for generated files.")
  arg_parser.add_argument("--file",
                          required=True,
                          help="File name of the generated file.")
  arg_parser.add_argument(
      "--allow_list_name",
      required=True,
      help="Name of the variant / enum list in the histograms.xml file.")
  arg_parser.add_argument("--namespace",
                          required=True,
                          help="Namespace of the allow list array.")
  arg_parser.add_argument("--tag",
                          required=True,
                          help="XML tag name of either 'enum' or 'variant'.")
  arg_parser.add_argument("--input",
                          help="Path to .xml file with histogram descriptions.")
  return arg_parser.parse_args()


def main():
  arguments = _ParseArguments()
  _GenerateFile(arguments)


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