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

"""Runs checks on the files defining tests.

This performs the following checks:
* Checks that any entry in gn_isolate_map.pyl is referenced by some
  builder (modulo targets known to be used by builders in other projects
  or via other mechanisms).
* Checks that any target referenced by a builder is defined in
  gn_isolate_map.pyl (module magic targets).
"""

import argparse
import ast
import glob
import json
import os
import sys


THIS_DIR = os.path.dirname(os.path.abspath(__file__))


SKIP_GN_ISOLATE_MAP_TARGETS = {
    # This target is magic and not present in gn_isolate_map.pyl.
    'all',
    'remoting/host:host',

    # These targets are only used by script tests
    'traffic_annotation_proto',

    # These targets are listed only in build-side recipes.
    'captured_sites_interactive_tests',
    'chrome_official_builder_no_unittests',
    'mini_installer',
    'previous_version_mini_installer',

    # These are used elsewhere.
    'media_router_e2e_tests',
    'traffic_annotation_auditor_dependencies',
    'vr_common_perftests',
    'vrcore_fps_test',

    # These are only run on V8 CI.
    'postmortem-metadata',

    # These are only for developer convenience and not on any bots.
    'telemetry_gpu_integration_test_scripts_only',

    # These are defined by an android internal gn_isolate_map.pyl file.
    'resource_sizes_trichrome_google',
    'resource_sizes_system_webview_google_bundle',
    'system_webview_google_apk',
    'trichrome_google_64_32_minimal_apks',
    'trichrome_google_64_minimal_apks',

    # These are used by https://www.chromium.org/developers/cluster-telemetry.
    'ct_telemetry_perf_tests_without_chrome',
}


class Error(Exception):
  """Processing error."""


def check_file(filepath, ninja_targets, ninja_targets_seen):
  """Processes a json file describing what tests should be run for each recipe.

  Raises an Error if the file doesn't pass checks.
  """
  filename = os.path.basename(filepath)
  with open(filepath) as f:
    content = f.read()
  try:
    config = json.loads(content)
  except ValueError as e:
    raise Error('Exception raised while checking %s: %s' % (filepath, e)) from e

  for builder, data in sorted(config.items()):
    if not isinstance(data, dict):
      raise Error('%s: %s is broken: %s' % (filename, builder, data))
    if ('gtest_tests' not in data and
        'isolated_scripts' not in data and
        'additional_compile_targets' not in data and
        'instrumentation_tests' not in data):
      continue

    for d in data.get('junit_tests', []):
      test = d['test']
      if (test not in ninja_targets and
          test not in SKIP_GN_ISOLATE_MAP_TARGETS):
        raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl' %
                    (filename, builder, test))
      if test in ninja_targets:
        ninja_targets_seen.add(test)

    for target in data.get('additional_compile_targets', []):
      if (target not in ninja_targets and
          target not in SKIP_GN_ISOLATE_MAP_TARGETS):
        raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl' %
                    (filename, builder, target))
      if target in ninja_targets:
        ninja_targets_seen.add(target)

    seen = set()
    for d in data.get('gtest_tests', []):
      test = d['test']
      if (test not in ninja_targets and
          test not in SKIP_GN_ISOLATE_MAP_TARGETS):
        raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' %
                    (filename, builder, test))
      if test in ninja_targets:
        ninja_targets_seen.add(test)

      name = d.get('name', d['test'])
      if name in seen:
        raise Error('%s: %s / %s is listed multiple times.' %
                    (filename, builder, name))
      seen.add(name)

    for d in data.get('isolated_scripts', []):
      name = d['test']
      if (name not in ninja_targets and
          name not in SKIP_GN_ISOLATE_MAP_TARGETS):
        raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' %
                    (filename, builder, name))
      if name in ninja_targets:
        ninja_targets_seen.add(name)

    for d in (data.get('instrumentation_tests', []) +
              data.get('skylab_tests', [])):
      name = d['test']
      if (name not in ninja_targets and
          name not in SKIP_GN_ISOLATE_MAP_TARGETS):
        raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' %
                    (filename, builder, name))
      if name in ninja_targets:
        ninja_targets_seen.add(name)


def main():
  parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__)
  parser.parse_args()

  gn_isolate_map_pyl_path = os.path.normpath(
      os.path.join(THIS_DIR, '..', '..', 'infra', 'config', 'generated',
                   'testing', 'gn_isolate_map.pyl'))
  with open(gn_isolate_map_pyl_path) as fp:
    gn_isolate_map = ast.literal_eval(fp.read())
    ninja_targets = {k: v['label'] for k, v in gn_isolate_map.items()}

  try:
    ninja_targets_seen = set()
    for filepath in glob.glob(os.path.join(THIS_DIR, '*.json')):
      # This file is formatted differently from other json files
      if 'autoshard_exceptions' in filepath:
        continue
      check_file(filepath, ninja_targets, ninja_targets_seen)

    skip_targets = [k for k, v in gn_isolate_map.items() if
                    ('skip_usage_check' in v and v['skip_usage_check'])]
    extra_targets = (set(ninja_targets) - set(skip_targets) -
                     ninja_targets_seen - SKIP_GN_ISOLATE_MAP_TARGETS)
    if extra_targets:
      if len(extra_targets) > 1:
        extra_targets_str = ', '.join(extra_targets) + ' are'
      else:
        extra_targets_str = list(extra_targets)[0] + ' is'
      raise Error('%s listed in gn_isolate_map.pyl but not in any .json '
                  'files' % extra_targets_str)

    return 0
  except Error as e:
    sys.stderr.write('%s\n' % e)
    return 1


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