chromium/chromecast/tools/build/generate_test_lists.py

#!/usr/bin/env python
# Copyright 2014 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helper script to generate unit test lists for the Chromecast build scripts.
"""

import glob
import json
import optparse
import os
import sys


def GetTestNames(test_files_dir):
  """Returns test names specified in the *.tests files in |test_files_dir|."""
  test_files = sorted(glob.glob(test_files_dir + "/*.tests"))
  test_names = set()
  for test_filename in test_files:
    with open(test_filename, "r") as test_file:
      for test_file_line in test_file:
        # Binary name may be a simple test target (cast_net_unittests) or be a
        # qualified gyp path (../base.gyp:base_unittests).
        test_name = test_file_line.split(":")[-1].strip()
        test_names.add(test_name)
  return test_names


def GetTestFilters(test_files_dir, test_names, include_filters):
  """Returns filters specified in the *.filters files in |test_files_dir|."""
  # GYP targets may provide a numbered priority for the filename. Sort to
  # use that priority.
  filter_files = sorted(glob.glob(test_files_dir + "/*.filters"))
  test_filters = {}
  if include_filters:
    for filter_filename in filter_files:
      with open(filter_filename, "r") as filter_file:
        for filter_line in filter_file:
          (test_name, test_filter) = filter_line.strip().split(" ", 1)

          if test_name not in test_names:
            raise Exception("Filter found for unknown target: " + test_name)

          # Note: This may overwrite a previous rule. This is okay, since higher
          # priority files are evaluated after lower priority files.
          test_filters[test_name] = test_filter

  return test_filters


def CombineList(test_files_dir, list_output_file, include_filters,
                additional_runtime_options):
  """Writes a unit test file in a format compatible for Chromecast scripts.

  If include_filters is True, uses filters to create a test runner list
  and also include additional options, if any.
  Otherwise, creates a list only of the tests to build.

  Args:
    test_files_dir: Path to the intermediate directory containing tests/filters.
    list_output_file: Path to write the unit test file out to.
    include_filters: Whether or not to include the filters when generating
        the test list.
    additional_runtime_options: Arguments to be applied to all tests.  These are
        applied before filters (so test-specific filters take precedence).

  Raises:
    Exception: if filter is found for an unknown target.
  """
  test_names = GetTestNames(test_files_dir)
  test_filters = GetTestFilters(test_files_dir, test_names, include_filters)
  test_commands = [
      "{} {} {}".format(test_name,
                        additional_runtime_options or "",
                        test_filters.get(test_name, ""))
      for test_name in test_names
  ]

  with open(list_output_file, "w") as f:
    f.write("\n".join(sorted(test_commands)))


def CombineRuntimeDeps(test_files_dir, deps_output_file):
  """Writes a JSON file that lists the runtime dependecies for each test.

  The output will consist of a JSON dictionary where the keys are names of the
  unittests and the values are arrays of files and directories needed at runtime
  by the unittest. Of note, the unittest itself is always listed as a runtime
  dependency of itself.

  The paths are all relative to the root output directory (where the unittest
  binaries live).

  {
    "base_unittests": ["./base_unittests", "../../base/test/data/"],
    "cast_media_unittests": [...],
    ...
  }

  Args:
    test_files_dir: path to the intermediate directory containing the invidual
        runtime deps files.
    deps_output_file: Path to write the JSON file out to.
  """
  test_names = GetTestNames(test_files_dir)
  runtime_deps = {}
  runtime_deps_dir = os.path.join(test_files_dir, "runtime_deps")
  for runtime_deps_file in glob.glob(runtime_deps_dir + "/*_runtime_deps.txt"):
    test_name = os.path.basename(runtime_deps_file).replace(
        "_runtime_deps.txt", "")
    if test_name not in test_names:
      continue

    with open(runtime_deps_file, "r") as f:
      runtime_deps[test_name] = [dep.strip() for dep in f]

  with open(deps_output_file, "w") as outfile:
    json.dump(
        runtime_deps, outfile, sort_keys=True, indent=2, separators=(",", ": "))


def CreateList(inputs, list_output_file):
  with open(list_output_file, "w") as f:
    f.write("\n".join(inputs))


def DoMain(argv):
  """Main method. Runs helper commands for generating unit test lists."""
  parser = optparse.OptionParser(
      """usage: %prog [<options>] <command> [<test names>]

      Valid commands:
          create_list       prints all given test names/args to a file, one line
                            per string
          pack_build        packs all test files from the given output directory
                            into a single test list file
          pack_run          packs all test and filter files from the given
                            output directory into a single test list file
      """)
  parser.add_option(
      "-o",
      action="store",
      dest="list_output_file",
      help="Output path in which to write the test list.")
  parser.add_option(
      "-d",
      action="store",
      dest="deps_output_file",
      help="Output path in which to write the runtime deps.")
  parser.add_option(
      "-t",
      action="store",
      dest="test_files_dir",
      help="Intermediate test list directory.")
  parser.add_option(
      "-a",
      action="store",
      dest="additional_runtime_options",
      help="Additional options applied to all tests.")
  options, inputs = parser.parse_args(argv)

  list_output_file = options.list_output_file
  deps_output_file = options.deps_output_file
  test_files_dir = options.test_files_dir
  additional_runtime_options = options.additional_runtime_options

  if len(inputs) < 1:
    parser.error("No command given.\n")
  command = inputs[0]
  test_names = inputs[1:]

  if not list_output_file:
    parser.error("Output path (-o) is required.\n")

  if command == "create_list":
    return CreateList(test_names, list_output_file)

  if command == "pack_build":
    if not test_files_dir:
      parser.error("pack_build require a test files directory (-t).\n")
    return CombineList(test_files_dir, list_output_file, False, None)

  if command == "pack_run":
    if not test_files_dir:
      parser.error("pack_run require a test files directory (-t).\n")
    if deps_output_file:
      CombineRuntimeDeps(test_files_dir, deps_output_file)
    return CombineList(test_files_dir, list_output_file, True,
                       additional_runtime_options)

  parser.error("Invalid command specified.")


if __name__ == "__main__":
  DoMain(sys.argv[1:])