chromium/testing/buildbot/generate_buildbot_json_unittest.py

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

"""Tests for generate_buildbot_json.py."""

import contextlib
import json
import os
import re
import unittest

import generate_buildbot_json
from pyfakefs import fake_filesystem_unittest

# pylint: disable=super-with-arguments

EMPTY_PYL_FILE = """\
{
}
"""

# Use this value to refer to the directory containing this code
# The tests use a fake filesystem and python filesystem calls are monkey-patched
# to use the fake filesystem, which affects abspath
THIS_DIR = os.path.dirname(os.path.abspath(__file__))


class TestCase(fake_filesystem_unittest.TestCase):
  def setUp(self):
    self.setUpPyfakefs()
    self.fs.cwd = THIS_DIR

    expectations_dir = os.path.join(THIS_DIR, 'unittest_expectations')
    self.output_dir = os.path.join(expectations_dir, self.id().split('.')[-1])

    # This allows reads into this directory to fallback to the real fs, which we
    # want so each test case's json can be checked-in.
    self.fs.add_real_directory(expectations_dir)

    self.set_args()

  def set_args(self, *args):
    self.args = generate_buildbot_json.BBJSONGenerator.parse_args((
        '--output-dir',
        self.output_dir,
    ) + args)

  def regen_test_json(self, fakebb):
    """Regenerates a unittest's json files.

    Useful when making sweeping changes to pyl inputs in many test cases. Can be
    used by simply inserting a call to this method in a unit test after
    initializing its FakeBBGen object.
    """
    contents = fakebb.generate_outputs()
    with fake_filesystem_unittest.Pause(self.fs):
      try:
        os.mkdir(self.output_dir)
      except FileExistsError:
        if not os.path.isdir(self.output_dir):
          raise

      fakebb.write_json_result(contents)


@contextlib.contextmanager
def dump_on_failure(fbb, dump=True):
  try:
    yield
  except:
    if dump:
      for l in fbb.printed_lines:
        print(l)
    raise

class FakeBBGen(generate_buildbot_json.BBJSONGenerator):
  def __init__(self,
               args,
               waterfalls,
               test_suites,
               luci_milo_cfg,
               project_pyl='{"validate_source_side_specs_have_builder": True}',
               exceptions=EMPTY_PYL_FILE,
               autoshard_exceptions=json.dumps({}),
               mixins=EMPTY_PYL_FILE,
               gn_isolate_map=EMPTY_PYL_FILE,
               variants=EMPTY_PYL_FILE):
    super(FakeBBGen, self).__init__(args)

    pyl_files_dir = args.pyl_files_dir or THIS_DIR
    infra_config_dir = args.infra_config_dir

    files = {
        args.waterfalls_pyl_path: waterfalls,
        args.test_suites_pyl_path: test_suites,
        args.test_suite_exceptions_pyl_path: exceptions,
        args.autoshard_exceptions_json_path: autoshard_exceptions,
        args.mixins_pyl_path: mixins,
        args.gn_isolate_map_pyl_path: gn_isolate_map,
        args.variants_pyl_path: variants,
        os.path.join(pyl_files_dir, 'gn_isolate_map2.pyl'):
        GPU_TELEMETRY_GN_ISOLATE_MAP,
        os.path.join(infra_config_dir, 'generated/project.pyl'): project_pyl,
        os.path.join(infra_config_dir, 'generated/luci/luci-milo.cfg'):
        luci_milo_cfg,
        os.path.join(infra_config_dir, 'generated/luci/luci-milo-dev.cfg'): '',
    }
    for path, content in files.items():
      if content is None:
        continue
      parent = os.path.abspath(os.path.dirname(path))
      if not os.path.exists(parent):
        os.makedirs(parent)
      with open(path, 'w') as f:
        f.write(content)

    self.printed_lines = []

  def print_line(self, line):
    self.printed_lines.append(line)

  # pragma pylint: disable=arguments-differ
  def check_output_file_consistency(self, verbose=False, dump=True):
    with dump_on_failure(self, dump=verbose and dump):
      super(FakeBBGen, self).check_output_file_consistency(verbose)
  # pragma pylint: enable=arguments-differ


FOO_GTESTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions':{
            'kvm': '1',
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_WITH_ENABLE_FEATURES_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
        'args': [
          '--enable-features=Baz',
        ],
      },
    },
  },
]
"""

FOO_CHROMEOS_TRIGGER_SCRIPT_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            "device_type": "foo_device",
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
        'os_type': 'chromeos',
      },
    },
  },
]
"""

FOO_LINUX_GTESTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'linux',
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

COMPOSITION_GTEST_SUITE_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

COMPOSITION_GTEST_SUITE_WITH_ARGS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'composition_tests',
        },
        'args': [
          '--this-is-an-argument',
        ],
      },
    },
  },
]
"""

FOO_ISOLATED_SCRIPTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'isolated_scripts': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_ISOLATED_SCRIPTS_WATERFALL_ANDROID = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'android',
        'test_suites': {
          'isolated_scripts': 'composition_tests',
        },
        'use_android_presentation': True,
      },
    },
  },
]
"""

FOO_SCRIPT_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'scripts': 'foo_scripts',
        },
      },
    },
  },
]
"""

FOO_SCRIPT_WATERFALL_MACHINE_FORBIDS_SCRIPT_TESTS = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'forbid_script_tests': True,
        'test_suites': {
          'scripts': 'foo_scripts',
        },
      },
    },
  },
]
"""

FOO_SCRIPT_WATERFALL_FORBID_SCRIPT_TESTS = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'forbid_script_tests': True,
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'scripts': 'foo_scripts',
        },
      },
    },
  },
]
"""

FOO_JUNIT_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'junit_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'win',
        'browser_config': 'release',
        'swarming': {
          'dimensions': {
            'gpu': '10de:1cb3',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL_ANDROID = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'android',
        'browser_config': 'android-chromium',
        'swarming': {
          'dimensions': {
            'device_type': 'bullhead',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL_ANDROID_WEBVIEW = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'android',
        'browser_config': 'not-a-real-browser',
        'swarming': {
          'dimensions': {
            'device_type': 'bullhead',
          },
        },
        'test_suites': {
          'android_webview_gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL_FUCHSIA = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'fuchsia',
        'browser_config': 'fuchsia-chrome',
        'swarming': {
          'dimensions': {
            'kvm': '1',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL_CAST_STREAMING = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'fuchsia',
        'browser_config': 'not-a-real-browser',
        'swarming': {
          'dimensions': {
            'kvm': '1',
          },
        },
        'test_suites': {
          'cast_streaming_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_WATERFALL_SKYLAB = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'chromeos',
        'browser_config': 'cros-chrome',
        'use_swarming': False,
        'swarming': {
          'some_key': 'some_value',
        },
        'test_suites': {
          'skylab_gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

NVIDIA_GPU_TELEMETRY_TEST_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'win',
        'browser_config': 'release',
        'swarming': {
          'dimensions': {
            'gpu': '10de:1cb3-26.21.14.3102',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

INTEL_GPU_TELEMETRY_TEST_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'win',
        'browser_config': 'release',
        'swarming': {
          'dimensions': {
            'gpu': '8086:5912-24.20.100.6286',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

INTEL_UHD_GPU_TELEMETRY_TEST_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'win',
        'browser_config': 'release',
        'swarming': {
          'dimensions': {
            'gpu': '8086:3e92-24.20.100.6286',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'composition_tests',
        },
      },
    },
  },
]
"""

GPU_TELEMETRY_TEST_VARIANTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'win',
        'browser_config': 'release',
        'swarming': {
          'dimensions': {
            'gpu': '8086:3e92-24.20.100.6286',
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gpu_telemetry_tests': 'matrix_tests',
        },
      },
    },
  },
]
"""

UNKNOWN_TEST_SUITE_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'baz_tests',
        },
      },
    },
  },
]
"""

UNKNOWN_TEST_SUITE_TYPE_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'foo_tests',
          'foo_test_type': 'foo_tests',
        },
      },
    },
  },
]
"""

ANDROID_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Android Builder': {
        'additional_compile_targets': [
          'bar_test',
        ],
      },
      'Fake Android K Tester': {
        'additional_compile_targets': [
          'bar_test',
        ],
        'swarming': {
          'dimensions': {
            'device_os': 'KTU84P',
            'device_type': 'hammerhead',
            'os': 'Android',
          },
        },
        'os_type': 'android',
        'skip_merge_script': True,
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
      'Fake Android L Tester': {
        'swarming': {
          'dimensions': {
            'device_os': 'LMY41U',
            'device_os_type': 'user',
            'device_type': 'hammerhead',
            'os': 'Android',
          },
        },
        'os_type': 'android',
        'skip_merge_script': True,
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
      'Fake Android M Tester': {
        'swarming': {
          'dimensions': {
            'device_os': 'MMB29Q',
            'device_type': 'bullhead',
            'os': 'Android',
          },
        },
        'os_type': 'android',
        'use_swarming': False,
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

UNKNOWN_BOT_GTESTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Unknown Bot': {
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

MATRIX_GTEST_SUITE_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'matrix_tests',
        },
      },
    },
  },
]
"""

MATRIX_GTEST_SUITE_WATERFALL_MIXINS = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['dimension_mixin'],
        'test_suites': {
          'gtest_tests': 'matrix_tests',
        },
      },
    },
  },
]
"""

FOO_TEST_SUITE = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'swarming': {
          'dimensions': {
            'integrity': 'high',
            'os': 'Linux',
          },
          'expiration': 120,
        },
      },
    },
  },
}
"""

FOO_TEST_SUITE_ANDROID_SWARMING = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'android_swarming': {
          'shards': 100,
        },
      },
    },
  },
}
"""

FOO_TEST_SUITE_NO_DIMENSIONS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
      },
    },
  },
}
"""

FOO_TEST_SUITE_NOT_SORTED = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
      'a_test': {},
    },
  },
}
"""

FOO_TEST_SUITE_WITH_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'args': [
          '--c_arg',
        ],
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_SWARMING_DIMENSION_SETS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'swarming': {
          'dimension_sets': [
            {
              'foo': 'bar',
            },
          ],
        },
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_NAME = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'name': 'bar_test',
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_ISOLATE_NAME = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'isolate_name': 'bar_test',
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_SWARMING_NAMED_CACHES = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'swarming': {
          'named_caches': [
            {
              'name': 'cache_in_test',
              'file': 'cache_in_test_file',
            },
          ],
        },
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_LINUX_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'linux_args': [
          '--no-xvfb',
        ],
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_ENABLE_FEATURES = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'args': [
          '--enable-features=Foo,Bar',
        ],
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_REMOVE_WATERFALL_MIXIN = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'remove_mixins': ['waterfall_mixin'],
        'swarming': {
          'dimensions': {
            'integrity': 'high',
          },
          'expiration': 120,
        },
      },
    },
  },
}
"""

FOO_TEST_SUITE_WITH_REMOVE_BUILDER_MIXIN = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'remove_mixins': ['builder_mixin'],
        'swarming': {
          'dimensions': {
            'integrity': 'high',
          },
          'expiration': 120,
        },
      },
    },
  },
}
"""

FOO_SCRIPT_SUITE = """\
{
  'basic_suites': {
    'foo_scripts': {
      'foo_test': {
        'script': 'foo.py',
      },
      'bar_test': {
        'script': 'bar.py',
      },
    },
  },
}
"""

GOOD_COMPOSITION_TEST_SUITES = """\
{
  'basic_suites': {
    'bar_tests': {
      'bar_test': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
      },
    },
    'foo_tests': {
      'foo_test': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
      },
    },
  },
  'compound_suites': {
    'composition_tests': [
      'foo_tests',
      'bar_tests',
    ],
  },
}
"""

BAD_COMPOSITION_TEST_SUITES = """\
{
  'basic_suites': {
    'bar_tests': {},
    'foo_tests': {},
  },
  'compound_suites': {
    'buggy_composition_tests': [
      'bar_tests',
    ],
    'composition_tests': [
      'foo_tests',
      'buggy_composition_tests',
    ],
  },
}
"""

CONFLICTING_COMPOSITION_TEST_SUITES = """\
{
  'basic_suites': {
    'bar_tests': {
      'baz_tests': {
        'args': [
          '--bar',
        ],
      }
    },
    'foo_tests': {
      'baz_tests': {
        'args': [
          '--foo',
        ],
      }
    },
  },
  'compound_suites': {
    'foobar_tests': [
      'foo_tests',
      'bar_tests',
    ],
  },
}
"""

DUPLICATES_COMPOSITION_TEST_SUITES = """\
{
  'basic_suites': {
    'bar_tests': {},
    'buggy_composition_tests': {},
    'foo_tests': {},
  },
  'compound_suites': {
    'bar_tests': [
      'foo_tests',
    ],
    'composition_tests': [
      'foo_tests',
      'buggy_composition_tests',
    ],
  },
}
"""

SCRIPT_SUITE = """\
{
  'basic_suites': {
    'foo_scripts': {
      'foo_test': {
        'script': 'foo.py',
      },
    },
  },
}
"""

UNREFED_TEST_SUITE = """\
{
  'basic_suites': {
    'bar_tests': {},
    'foo_tests': {},
  },
}
"""

REUSING_TEST_WITH_DIFFERENT_NAME = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
      'variation_test': {
        'args': [
          '--variation',
        ],
        'test': 'foo_test',
      },
    },
  },
}
"""

COMPOSITION_SUITE_WITH_TELEMETRY_TEST_WITH_INVALID_NAME = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo': {
        'telemetry_test_name': 'foo',
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
      },
    },
  },
  'compound_suites': {
    'composition_tests': [
      'foo_tests',
    ],
  },
}
"""

COMPOSITION_SUITE_WITH_TELEMETRY_TEST = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_tests': {
        'telemetry_test_name': 'foo',
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
          'shards': 2,
        },
      },
    },
    'bar_tests': {
      'bar_test': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
      },
    },
  },
  'compound_suites': {
    'composition_tests': [
      'foo_tests',
      'bar_tests',
    ],
  },
}
"""

COMPOSITION_SUITE_WITH_GPU_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_tests': {
        'telemetry_test_name': 'foo',
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'args': [
          '--gpu-vendor-id',
          '${gpu_vendor_id}',
          '--gpu-device-id',
          '${gpu_device_id}',
        ],
      },
    },
    'bar_tests': {
      'bar_test': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
      },
    },
  },
  'compound_suites': {
    'composition_tests': [
      'foo_tests',
      'bar_tests',
    ],
  },
}
"""

COMPOSITION_SUITE_WITH_PIXEL_AND_FILTER = """\
{
  'basic_suites': {
    'foo_tests': {
      'pixel': {
        'args': [
          '--git-revision=aaaa',
          '--test-filter=bar',
        ],
      },
    },
    'bar_tests': {
      'bar_test': {},
    },
  },
  'compound_suites': {
    'composition_tests': [
      'foo_tests',
      'bar_tests',
    ],
  },
}
"""

GTEST_AS_ISOLATED_SCRIPT_SUITE = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'script': 'foo.py',
        'use_isolated_scripts_api': True,
      },
    },
  },
}
"""

SCRIPT_WITH_ARGS_EXCEPTIONS = """\
{
  'foo_test': {
    'modifications': {
      'Fake Tester': {
        'args': ['--fake-arg'],
      },
    },
  },
}
"""

SCRIPT_WITH_ARGS_SWARMING_EXCEPTIONS = """\
{
  'foo_test': {
    'modifications': {
      'Fake Tester': {
        'swarming': {
          'value': 'exception',
        },
      },
    },
  },
}
"""

AUTOSHARD_EXCEPTIONS = json.dumps({
    'chromium.test': {
        'Fake Tester': {
            'foo_test': {
                'shards': 20,
            },
            'foo_test_apk': {
                'shards': 2,
            },
            'swarming_test a_variant': {
                'shards': 10,
            },
        }
    },
})

NO_BAR_TEST_EXCEPTIONS = """\
{
  'bar_test': {
    'remove_from': [
      'Fake Tester',
    ]
  }
}
"""

EMPTY_BAR_TEST_EXCEPTIONS = """\
{
  'bar_test': {
  }
}
"""

EXCEPTIONS_SORTED = """\
{
  'suite_c': {
    'modifications': {
      'Fake Tester': {
        'foo': 'bar',
      },
    },
  },
  'suite_d': {
    'modifications': {
      'Fake Tester': {
        'foo': 'baz',
      },
    },
  },
}
"""

EXCEPTIONS_UNSORTED = """\
{
  'suite_d': {
    'modifications': {
      'Fake Tester': {
        'foo': 'baz',
      },
    },
  },
  'suite_c': {
    'modifications': {
      'Fake Tester': {
        'foo': 'bar',
      },
    },
  },
}
"""

EXCEPTIONS_PER_TEST_UNSORTED = """\
{
  'suite_d': {
    'modifications': {
      'Other Tester': {
        'foo': 'baz',
      },
      'Fake Tester': {
        'foo': 'baz',
      },
    },
  },
}
"""

EXCEPTIONS_DUPS_REMOVE_FROM = """\
{
  'suite_d': {
    'remove_from': [
      'Fake Tester',
      'Fake Tester',
    ],
    'modifications': {
      'Fake Tester': {
        'foo': 'baz',
      },
    },
  },
}
"""

FOO_TEST_MODIFICATIONS = """\
{
  'foo_test': {
    'modifications': {
      'Fake Tester': {
        'args': [
          '--bar',
        ],
        'swarming': {
          'hard_timeout': 600,
        },
      },
    },
  }
}
"""

FOO_TEST_EXPLICIT_NONE_EXCEPTIONS = """\
{
  'foo_test': {
    'modifications': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            'integrity': None,
          },
        },
      },
    },
  },
}
"""

NONEXISTENT_REMOVAL = """\
{
  'foo_test': {
    'remove_from': [
      'Nonexistent Tester',
    ]
  }
}
"""

NONEXISTENT_MODIFICATION = """\
{
  'foo_test': {
    'modifications': {
      'Nonexistent Tester': {
        'args': [],
      },
    },
  }
}
"""

LUCI_MILO_CFG = """\
consoles {
  builders {
    name: "buildbucket/luci.chromium.ci/Fake Tester"
  }
}
"""

LUCI_MILO_CFG_WATERFALL_SORTING = """\
consoles {
  builders {
    name: "buildbucket/luci.chromium.ci/Fake Tester"
    name: "buildbucket/luci.chromium.ci/Really Fake Tester"
  }
}
"""

TEST_SUITE_SORTING_WATERFALL = """
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
]
"""

TEST_SUITE_SORTED_WATERFALL = """
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.zz.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
]
"""

TEST_SUITE_UNSORTED_WATERFALL_1 = """
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.zz.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
]
"""

TEST_SUITE_UNSORTED_WATERFALL_2 = """
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.zz.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
      'Really Fake Tester': {
        'test_suites': {
          'gtest_tests': 'suite_a',
          'scripts': 'suite_b',
        },
      },
    },
  },
]
"""

# Note that the suites in basic_suites would be sorted after the suites in
# compound_suites. This is valid though, because each set of suites is sorted
# separately.
# suite_c is a 'gtest_tests' test
# suite_d is a 'scripts' test
TEST_SUITE_SORTED = """\
{
  'basic_suites': {
    'suite_c': {
      'suite_c': {},
    },
    'suite_d': {
      'script': {
        'script': 'suite_d.py',
      }
    },
  },
  'compound_suites': {
    'suite_a': [
      'suite_c',
    ],
    'suite_b': [
      'suite_d',
    ],
  },
}
"""

TEST_SUITE_UNSORTED_1 = """\
{
  'basic_suites': {
    'suite_d': {
      'a': 'b',
    },
    'suite_c': {
      'a': 'b',
    },
  },
  'compound_suites': {
    'suite_a': [
      'suite_c',
    ],
    'suite_b': [
      'suite_d',
    ],
  },
}
"""

TEST_SUITE_UNSORTED_2 = """\
{
  'basic_suites': {
    'suite_c': {
      'a': 'b',
    },
    'suite_d': {
      'a': 'b',
    },
  },
  'compound_suites': {
    'suite_b': [
      'suite_c',
    ],
    'suite_a': [
      'suite_d',
    ],
  },
}
"""
TEST_SUITE_UNSORTED_3 = """\
{
  'basic_suites': {
    'suite_d': {
      'a': 'b',
    },
    'suite_c': {
      'a': 'b',
    },
  },
  'compound_suites': {
    'suite_b': [
      'suite_c',
    ],
    'suite_a': [
      'suite_d',
    ],
  },
}
"""


TEST_SUITES_SYNTAX_ERROR = """\
{
  'basic_suites': {
    3: {
      'suite_c': {},
    },
  },
  'compound_suites': {},
}
"""

GN_ISOLATE_MAP="""\
{
  'foo_test': {
    'label': '//chrome/test:foo_test',
    'type': 'windowed_test_launcher',
  }
}
"""

GPU_TELEMETRY_GN_ISOLATE_MAP="""\
{
  'telemetry_gpu_integration_test': {
    'label': '//chrome/test:telemetry_gpu_integration_test',
    'type': 'script',
      }
}
"""

GPU_TELEMETRY_GN_ISOLATE_MAP_ANDROID = """\
{
  'telemetry_gpu_integration_test_android_chrome': {
    'label': '//chrome/test:telemetry_gpu_integration_test_android_chrome',
    'type': 'script',
      }
}
"""

GPU_TELEMETRY_GN_ISOLATE_MAP_ANDROID_WEBVIEW = """\
{
  'telemetry_gpu_integration_test_android_webview': {
    'label': '//chrome/test:telemetry_gpu_integration_test_android_webview',
    'type': 'script',
      }
}
"""

GPU_TELEMETRY_GN_ISOLATE_MAP_FUCHSIA = """\
{
  'telemetry_gpu_integration_test_fuchsia': {
    'label': '//chrome/test:telemetry_gpu_integration_test_fuchsia',
    'type': 'script',
      }
}
"""

GPU_TELEMETRY_GN_ISOLATE_MAP_CAST_STREAMING = """\
{
  'telemetry_gpu_integration_test_fuchsia': {
    'label': '//chrome/test:telemetry_gpu_integration_test_fuchsia',
    'type': 'script',
      }
}
"""

GN_ISOLATE_MAP_KEY_LABEL_MISMATCH="""\
{
  'foo_test': {
    'label': '//chrome/test:foo_test_tmp',
    'type': 'windowed_test_launcher',
  }
}
"""

GN_ISOLATE_MAP_USING_IMPLICIT_NAME="""\
{
  'foo_test': {
    'label': '//chrome/foo_test',
    'type': 'windowed_test_launcher',
  }
}
"""

class UnitTest(TestCase):

  def test_dimension_sets_causes_error(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_SWARMING_DIMENSION_SETS, LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                r'.*dimension_sets is no longer supported.*'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_name_causes_error(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_NAME, LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                r'.*\bname field is set\b.*'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_isolate_name_causes_error(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_ISOLATE_NAME, LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                r'.*\bisolate_name field is set\b.*'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_good_test_suites_are_ok(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL, FOO_TEST_SUITE,
                    LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_good_composition_test_suites_are_ok(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_bad_composition_test_suites_are_caught(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WATERFALL,
                    BAD_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'compound_suites may not refer to.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_composition_test_suites_no_conflicts(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WATERFALL,
                    CONFLICTING_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Conflicting test definitions.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_composition_test_suites_no_duplicate_names(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WATERFALL,
                    DUPLICATES_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                '.*may not duplicate basic test suite.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_unknown_test_suites_are_caught(self):
    fbb = FakeBBGen(self.args, UNKNOWN_TEST_SUITE_WATERFALL, FOO_TEST_SUITE,
                    LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Test suite baz_tests from machine.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_unknown_test_suite_types_are_caught(self):
    fbb = FakeBBGen(self.args, UNKNOWN_TEST_SUITE_TYPE_WATERFALL,
                    FOO_TEST_SUITE, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Unknown test suite type foo_test_type.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_unrefed_test_suite_caught(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL, UNREFED_TEST_SUITE,
                    LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                '.*unreferenced.*bar_tests.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_good_waterfall_output(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_reusing_gtest_targets(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    REUSING_TEST_WITH_DIFFERENT_NAME,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_load_multiple_isolate_map_files_with_duplicates(self):
    self.args.isolate_map_files = [self.args.gn_isolate_map_pyl_path]
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    REUSING_TEST_WITH_DIFFERENT_NAME,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Duplicate targets in isolate map files.*'):
      fbb.load_configuration_files()

  def test_load_multiple_isolate_map_files_without_duplicates(self):
    self.args.isolate_map_files = ['gn_isolate_map2.pyl']
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    REUSING_TEST_WITH_DIFFERENT_NAME,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP)
    fbb.load_configuration_files()
    isolate_dict = {}
    isolate_map_1 = fbb.load_pyl_file(self.args.gn_isolate_map_pyl_path)
    isolate_map_2 = fbb.load_pyl_file('gn_isolate_map2.pyl')
    isolate_dict.update(isolate_map_1)
    isolate_dict.update(isolate_map_2)
    self.assertEqual(isolate_dict, fbb.gn_isolate_map)

  def test_gn_isolate_map_with_label_mismatch(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP_KEY_LABEL_MISMATCH)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'key name.*foo_test.*label.*'
                                'foo_test_tmp.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gn_isolate_map_using_implicit_gn_name(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP_USING_IMPLICIT_NAME)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Malformed.*//chrome/foo_test.*for key.*'
        'foo_test.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_noop_exception_does_nothing(self):
    fbb = FakeBBGen(self.args,
                    COMPOSITION_GTEST_SUITE_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=EMPTY_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_test_arg_merges(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_MODIFICATIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_enable_features_arg_merges(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WITH_ENABLE_FEATURES_WATERFALL,
                    FOO_TEST_SUITE_WITH_ENABLE_FEATURES, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_linux_args(self):
    fbb = FakeBBGen(self.args, FOO_LINUX_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_LINUX_ARGS, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_test_filtering(self):
    fbb = FakeBBGen(self.args,
                    COMPOSITION_GTEST_SUITE_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_test_modifications(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_MODIFICATIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_test_with_explicit_none(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_EXPLICIT_NONE_EXCEPTIONS,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_isolated_script_tests(self):
    fbb = FakeBBGen(self.args,
                    FOO_ISOLATED_SCRIPTS_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_isolated_script_tests_android(self):
    fbb = FakeBBGen(self.args,
                    FOO_ISOLATED_SCRIPTS_WATERFALL_ANDROID,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_script_with_args(self):
    fbb = FakeBBGen(self.args,
                    FOO_SCRIPT_WATERFALL,
                    SCRIPT_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=SCRIPT_WITH_ARGS_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_script(self):
    fbb = FakeBBGen(self.args,
                    FOO_SCRIPT_WATERFALL,
                    FOO_SCRIPT_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_script_machine_forbids_scripts(self):
    fbb = FakeBBGen(self.args,
                    FOO_SCRIPT_WATERFALL_MACHINE_FORBIDS_SCRIPT_TESTS,
                    FOO_SCRIPT_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Attempted to generate a script test on tester.*'):
      fbb.check_output_file_consistency(verbose=True)

  def test_script_waterfall_forbids_scripts(self):
    fbb = FakeBBGen(self.args,
                    FOO_SCRIPT_WATERFALL_FORBID_SCRIPT_TESTS,
                    FOO_SCRIPT_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Attempted to generate a script test on tester.*'):
      fbb.check_output_file_consistency(verbose=True)

  def test_junit_tests(self):
    fbb = FakeBBGen(self.args,
                    FOO_JUNIT_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_test_with_invalid_name(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST_WITH_INVALID_NAME,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'telemetry test names must end with test or tests.*'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_android(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL_ANDROID,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP_ANDROID)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_android_webview(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL_ANDROID_WEBVIEW,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP_ANDROID_WEBVIEW)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_fuchsia(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL_FUCHSIA,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP_FUCHSIA)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_cast_streaming(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL_CAST_STREAMING,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP_CAST_STREAMING)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_skylab(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_WATERFALL_SKYLAB,
                    COMPOSITION_SUITE_WITH_TELEMETRY_TEST,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nvidia_gpu_telemetry_tests(self):
    fbb = FakeBBGen(self.args,
                    NVIDIA_GPU_TELEMETRY_TEST_WATERFALL,
                    COMPOSITION_SUITE_WITH_GPU_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_intel_gpu_telemetry_tests(self):
    fbb = FakeBBGen(self.args,
                    INTEL_GPU_TELEMETRY_TEST_WATERFALL,
                    COMPOSITION_SUITE_WITH_GPU_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gpu_telemetry_tests_with_variants(self):
    fbb = FakeBBGen(self.args,
                    GPU_TELEMETRY_TEST_VARIANTS_WATERFALL,
                    MATRIX_COMPOUND_MIXED_VARIANTS_REF,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP,
                    variants=MULTI_VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_intel_uhd_gpu_telemetry_tests(self):
    fbb = FakeBBGen(self.args,
                    INTEL_UHD_GPU_TELEMETRY_TEST_WATERFALL,
                    COMPOSITION_SUITE_WITH_GPU_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_gtest_as_isolated_Script(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    GTEST_AS_ISOLATED_SCRIPT_SUITE,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_ungenerated_output_files_are_caught(self):
    fbb = FakeBBGen(self.args,
                    COMPOSITION_GTEST_SUITE_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    exceptions=NO_BAR_TEST_EXCEPTIONS)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_output_file_consistency(verbose=True, dump=False)
    joined_lines = ' '.join(fbb.printed_lines)
    self.assertRegex(
        joined_lines, 'File chromium.test.json did not have the following'
        ' expected contents:.*')
    self.assertRegex(joined_lines, r'.*--- expected.*')
    self.assertRegex(joined_lines, r'.*\+\+\+ current.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_android_output_options(self):
    fbb = FakeBBGen(self.args, ANDROID_WATERFALL, FOO_TEST_SUITE, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_android_swarming(self):
    fbb = FakeBBGen(self.args, ANDROID_WATERFALL,
                    FOO_TEST_SUITE_ANDROID_SWARMING, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nonexistent_removal_raises(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=NONEXISTENT_REMOVAL)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'The following nonexistent machines.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nonexistent_modification_raises(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=NONEXISTENT_MODIFICATION)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'The following nonexistent machines.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_waterfall_args(self):
    fbb = FakeBBGen(self.args, COMPOSITION_GTEST_SUITE_WITH_ARGS_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_chromeos_trigger_script_output(self):
    fbb = FakeBBGen(self.args, FOO_CHROMEOS_TRIGGER_SCRIPT_WATERFALL,
                    FOO_TEST_SUITE, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_relative_pyl_file_dir(self):
    self.set_args('--pyl-files-dir=relative/path')
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    REUSING_TEST_WITH_DIFFERENT_NAME,
                    LUCI_MILO_CFG,
                    gn_isolate_map=GN_ISOLATE_MAP)
    fbb.check_input_file_consistency(verbose=True)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nonexistent_bot_raises(self):
    fbb = FakeBBGen(self.args, UNKNOWN_BOT_GTESTS_WATERFALL, FOO_TEST_SUITE,
                    LUCI_MILO_CFG)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nonexistent_bot_raises_when_no_project_pyl_exists(self):
    fbb = FakeBBGen(self.args,
                    UNKNOWN_BOT_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    project_pyl=None)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_nonexistent_bot_does_not_raise_when_validation_disabled(self):
    fbb = FakeBBGen(
        self.args,
        UNKNOWN_BOT_GTESTS_WATERFALL,
        FOO_TEST_SUITE,
        LUCI_MILO_CFG,
        project_pyl='{"validate_source_side_specs_have_builder": False}')
    fbb.check_input_file_consistency(verbose=True)

  def test_waterfalls_must_be_sorted(self):
    fbb = FakeBBGen(self.args, TEST_SUITE_SORTED_WATERFALL, TEST_SUITE_SORTED,
                    LUCI_MILO_CFG_WATERFALL_SORTING)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args, TEST_SUITE_UNSORTED_WATERFALL_1,
                    TEST_SUITE_SORTED, LUCI_MILO_CFG_WATERFALL_SORTING)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        ('The following files have invalid keys: ' +
         re.escape(self.args.waterfalls_pyl_path)),
    ):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = '\n'.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\+ chromium\..*test.*')
    self.assertRegex(joined_lines, r'.*\- chromium\..*test.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args, TEST_SUITE_UNSORTED_WATERFALL_2,
                    TEST_SUITE_SORTED, LUCI_MILO_CFG_WATERFALL_SORTING)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        ('The following files have invalid keys: ' +
         re.escape(self.args.waterfalls_pyl_path)),
    ):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = ' '.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\+.*Fake Tester.*')
    self.assertRegex(joined_lines, r'.*\-.*Fake Tester.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_test_suite_exceptions_must_be_sorted(self):
    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_SORTED)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_DUPS_REMOVE_FROM)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = ' '.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\- Fake Tester.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_test_suite_exceptions_no_dups_remove_from(self):
    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_SORTED)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_PER_TEST_UNSORTED)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = ' '.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\+ Fake Tester.*')
    self.assertRegex(joined_lines, r'.*\- Fake Tester.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_test_suite_exceptions_per_test_must_be_sorted(self):
    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_SORTED)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args,
                    TEST_SUITE_SORTING_WATERFALL,
                    TEST_SUITE_SORTED,
                    LUCI_MILO_CFG,
                    exceptions=EXCEPTIONS_UNSORTED)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = ' '.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\+ suite_.*')
    self.assertRegex(joined_lines, r'.*\- suite_.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_test_suites_must_be_sorted(self):
    fbb = FakeBBGen(self.args, TEST_SUITE_SORTING_WATERFALL, TEST_SUITE_SORTED,
                    LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    for unsorted in (
        TEST_SUITE_UNSORTED_1,
        TEST_SUITE_UNSORTED_2,
        TEST_SUITE_UNSORTED_3,
    ):
      fbb = FakeBBGen(self.args, TEST_SUITE_SORTING_WATERFALL, unsorted,
                      LUCI_MILO_CFG)
      with self.assertRaises(generate_buildbot_json.BBGenErr):
        fbb.check_input_file_consistency(verbose=True)
      joined_lines = ' '.join(fbb.printed_lines)
      self.assertRegex(joined_lines, r'.*\+ suite_.*')
      self.assertRegex(joined_lines, r'.*\- suite_.*')
      fbb.printed_lines = []
      self.assertFalse(fbb.printed_lines)


FOO_GTESTS_WATERFALL_MIXIN_WATERFALL = """\
[
  {
    'mixins': ['waterfall_mixin'],
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_BUILDER_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['builder_mixin'],
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_LINUX_GTESTS_BUILDER_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'os_type': 'linux',
        'mixins': ['builder_mixin'],
        'swarming': {
          'dimensions': {
            'os': 'Linux',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_BUILDER_MIXIN_NON_SWARMING_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['random_mixin'],
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_DIMENSIONS_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['dimension_mixin'],
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GPU_TELEMETRY_TEST_DIMENSIONS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['dimension_mixin'],
        'os_type': 'win',
        'browser_config': 'release',
        'test_suites': {
          'gpu_telemetry_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

# Swarming mixins must be a list, a single string is not allowed.
FOO_GTESTS_INVALID_LIST_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': 'dimension_mixin',
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""
FOO_GTESTS_INVALID_NOTFOUND_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'mixins': ['nonexistant'],
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_TEST_MIXIN_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'mixins': ['waterfall_mixin'],
    'machines': {
      'Fake Tester': {
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_GTESTS_SORTING_MIXINS_WATERFALL = """\
[
  {
    'mixins': ['a_mixin', 'b_mixin', 'c_mixin'],
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'swarming': {
          'dimensions': {
            'kvm': '1',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

FOO_TEST_SUITE_WITH_MIXIN = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'swarming': {
          'dimensions': {
            'integrity': 'high',
            'os': 'Linux',
          },
          'expiration': 120,
        },
        'mixins': ['test_mixin'],
      },
    },
  },
}
"""

MIXIN_ARGS = """\
{
  'builder_mixin': {
    'args': [],
  },
}
"""

MIXIN_ARGS_NOT_LIST = """\
{
  'builder_mixin': {
    'args': 'I am not a list',
  },
}
"""

MIXIN_LINUX_ARGS = """\
{
  'builder_mixin': {
    'args': [ '--mixin-argument' ],
    'linux_args': [ '--linux-mixin-argument' ],
  },
}
"""

MIXIN_APPEND = """\
{
  'builder_mixin': {
    '$mixin_append': {
      'args': [ '--mixin-argument' ],
    },
  },
}
"""

# These mixins are invalid; if passed to check_input_file_consistency, they will
# fail. These are used for output file consistency checks.
SWARMING_MIXINS = """\
{
  'builder_mixin': {
    'swarming': {
      'value': 'builder',
    },
  },
  'dimension_mixin': {
    'swarming': {
      'dimensions': {
        'iama': 'mixin',
      },
    },
  },
  'random_mixin': {
    'value': 'random',
  },
  'test_mixin': {
    'swarming': {
      'value': 'test',
    },
  },
  'waterfall_mixin': {
    'swarming': {
      'value': 'waterfall',
    },
  },
}
"""

SWARMING_NAMED_CACHES = """\
{
  'builder_mixin': {
    'swarming': {
      'named_caches': [
        {
          'name': 'cache',
          'file': 'cache_file',
        },
      ],
    },
  },
}
"""

SWARMING_MIXINS_DUPLICATED = """\
{
  'builder_mixin': {
    'value': 'builder',
  },
  'builder_mixin': {
    'value': 'builder',
  },
}
"""

SWARMING_MIXINS_UNSORTED = """\
{
  'b_mixin': {
    'b': 'b',
  },
  'a_mixin': {
    'a': 'a',
  },
  'c_mixin': {
    'c': 'c',
  },
}
"""

SWARMING_MIXINS_SORTED = """\
{
  'a_mixin': {
    'a': 'a',
  },
  'b_mixin': {
    'b': 'b',
  },
  'c_mixin': {
    'c': 'c',
  },
}
"""


class MixinTests(TestCase):
  """Tests for the mixins feature."""
  def test_mixins_must_be_sorted(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_SORTING_MIXINS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_SORTING_MIXINS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_UNSORTED)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = '\n'.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\+ ._mixin.*')
    self.assertRegex(joined_lines, r'.*\- ._mixin.*')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_waterfall(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL_MIXIN_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_waterfall_exception_overrides(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL_MIXIN_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=SCRIPT_WITH_ARGS_SWARMING_EXCEPTIONS,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_autoshard_exceptions(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL_MIXIN_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=SCRIPT_WITH_ARGS_SWARMING_EXCEPTIONS,
                    autoshard_exceptions=AUTOSHARD_EXCEPTIONS,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_autoshard_exceptions_variant_names(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TEST_WITH_TEST_KEY,
                    LUCI_MILO_CFG,
                    autoshard_exceptions=AUTOSHARD_EXCEPTIONS,
                    variants=VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_builder(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_builder_non_swarming(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_NON_SWARMING_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_test_suite(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_dimension(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_DIMENSIONS_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_dimension_gpu(self):
    fbb = FakeBBGen(self.args,
                    FOO_GPU_TELEMETRY_TEST_DIMENSIONS_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS,
                    gn_isolate_map=GPU_TELEMETRY_GN_ISOLATE_MAP)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_unreferenced(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                '.*mixins are unreferenced.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_unused(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_INVALID_NOTFOUND_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_list(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_INVALID_LIST_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    with self.assertRaises(generate_buildbot_json.BBGenErr):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)


  def test_no_duplicate_keys(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_DUPLICATED)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        ('The following files have invalid keys: ' +
         re.escape(self.args.mixins_pyl_path)),
    ):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = '\n'.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\- builder_mixin')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_no_duplicate_keys_basic_test_suite(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL, FOO_TEST_SUITE_NOT_SORTED,
                    LUCI_MILO_CFG)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        ('The following files have invalid keys: ' +
         re.escape(self.args.test_suites_pyl_path)),
    ):
      fbb.check_input_file_consistency(verbose=True)
    joined_lines = '\n'.join(fbb.printed_lines)
    self.assertRegex(joined_lines, r'.*\- a_test')
    self.assertRegex(joined_lines, r'.*\+ a_test')
    fbb.printed_lines = []
    self.assertFalse(fbb.printed_lines)

  def test_type_assert_printing_help(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL, TEST_SUITES_SYNTAX_ERROR,
                    LUCI_MILO_CFG)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        f'Invalid \\.pyl file '
        f"'{re.escape(self.args.test_suites_pyl_path)}'.*",
    ):
      fbb.check_input_file_consistency(verbose=True)
    self.assertEqual(fbb.printed_lines, [
        f'== {self.args.test_suites_pyl_path} ==',
        '<snip>',
        '1 {',
        "2   'basic_suites': {",
        '--------------------------------------------------------------------'
        '------------',
        '3     3: {',
        '-------^------------------------------------------------------------'
        '------------',
        "4       'suite_c': {},",
        '5     },',
        '<snip>',
    ])

  def test_mixin_append(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    mixins=MIXIN_APPEND)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                r'.*\$mixin_append is no longer supported.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_swarming_named_caches(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_SWARMING_NAMED_CACHES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_NAMED_CACHES)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_args_field_not_list(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    mixins=MIXIN_ARGS_NOT_LIST)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                '"args" must be a list'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_args_field_merging(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    mixins=MIXIN_ARGS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_linux_args_field_merging(self):
    fbb = FakeBBGen(self.args,
                    FOO_LINUX_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    mixins=MIXIN_LINUX_ARGS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_remove_mixin_test_remove_waterfall(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_REMOVE_WATERFALL_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_remove_mixin_test_remove_builder(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_BUILDER_MIXIN_WATERFALL,
                    FOO_TEST_SUITE_WITH_REMOVE_BUILDER_MIXIN,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

TEST_SUITE_WITH_PARAMS = """\
{
  'basic_suites': {
    'bar_tests': {
      'bar_test': {
        'args': ['--no-xvfb'],
        'swarming': {
          'dimensions': {
            'device_os': 'NMF26U'
          },
        },
        'should_retry_with_patch': False,
      },
      'bar_test_test': {
        'swarming': {
          'dimensions': {
            'kvm': '1'
          },
          'hard_timeout': 1000
        },
        'should_retry_with_patch': True
      }
    },
    'foo_tests': {
      'foo_test_empty': {},
      'foo_test': {
        'args': [
          '--jobs=1',
          '--verbose'
        ],
        'swarming': {
          'dimensions': {
            'device_os': 'MMB29Q'
          },
          'hard_timeout': 1800
        }
      },
      'pls': {
        'swarming': {
        },
      },
    },
  },
  'compound_suites': {
    'composition_tests': [
        'foo_tests',
        'bar_tests',
    ],
  },
}
"""
TEST_QUERY_BOTS_OUTPUT = {
    'Fake Android M Tester': {
        'gtest_tests': [{
            'name': 'foo_test',
            'test': 'foo_test',
        }]
    },
    'Fake Android L Tester': {
        'gtest_tests': [{
            'test':
            'foo_test',
            'args': [
                '--gs-results-bucket=chromium-result-details',
                '--recover-devices'
            ],
            'merge': {
                'script': '//testing/merge_scripts/standard_gtest_merge.py'
            },
            'name':
            'foo_test',
            'swarming': {
                'dimensions': {
                    'device_os': 'LMY41U',
                    'device_os_type': 'user',
                    'device_type': 'hammerhead',
                    'os': 'Android'
                },
            }
        }]
    },
    'Fake Android K Tester': {
        'additional_compile_targets': ['bar_test'],
        'gtest_tests': [{
            'test':
            'foo_test',
            'args': [
                '--gs-results-bucket=chromium-result-details',
                '--recover-devices'
            ],
            'merge': {
                'script': '//testing/merge_scripts/standard_gtest_merge.py'
            },
            'name':
            'foo_test',
            'swarming': {
                'dimensions': {
                    'device_os': 'KTU84P',
                    'device_os_type': 'userdebug',
                    'device_type': 'hammerhead',
                    'os': 'Android',
                },
            }
        }]
    },
    'Android Builder': {
        'additional_compile_targets': ['bar_test']
    }
}
TEST_QUERY_BOTS_TESTS_OUTPUT = {
    'Fake Android M Tester': [{
        'name': 'foo_test',
        'test': 'foo_test',
    }],
    'Fake Android L Tester': [{
        'test':
        'foo_test',
        'args':
        ['--gs-results-bucket=chromium-result-details', '--recover-devices'],
        'merge': {
            'script': '//testing/merge_scripts/standard_gtest_merge.py'
        },
        'name':
        'foo_test',
        'swarming': {
            'dimensions': {
                'device_os': 'LMY41U',
                'device_os_type': 'user',
                'device_type': 'hammerhead',
                'os': 'Android'
            },
        }
    }],
    'Android Builder': [],
    'Fake Android K Tester': [{
        'test':
        'foo_test',
        'args':
        ['--gs-results-bucket=chromium-result-details', '--recover-devices'],
        'merge': {
            'script': '//testing/merge_scripts/standard_gtest_merge.py'
        },
        'name':
        'foo_test',
        'swarming': {
            'dimensions': {
                'device_os': 'KTU84P',
                'device_os_type': 'userdebug',
                'device_type': 'hammerhead',
                'os': 'Android'
            },
        }
    }]
}

TEST_QUERY_BOT_OUTPUT = {
    'additional_compile_targets': ['bar_test'],
    'gtest_tests': [
        {
            'test':
            'foo_test',
            'args': [
                '--gs-results-bucket=chromium-result-details',
                '--recover-devices',
            ],
            'merge': {
                'script': '//testing/merge_scripts/standard_gtest_merge.py',
            },
            'name':
            'foo_test',
            'swarming': {
                'dimensions': {
                    'device_os': 'KTU84P',
                    'device_os_type': 'userdebug',
                    'device_type': 'hammerhead',
                    'os': 'Android',
                },
            },
        },
    ],
}
TEST_QUERY_BOT_TESTS_OUTPUT = [{
    'test':
    'foo_test',
    'args':
    ['--gs-results-bucket=chromium-result-details', '--recover-devices'],
    'merge': {
        'script': '//testing/merge_scripts/standard_gtest_merge.py'
    },
    'name':
    'foo_test',
    'swarming': {
        'dimensions': {
            'device_os': 'LMY41U',
            'device_os_type': 'user',
            'device_type': 'hammerhead',
            'os': 'Android'
        },
    }
}]

TEST_QUERY_TESTS_OUTPUT = {
    'bar_test': {
        'name': 'bar_test',
        'swarming': {
            'dimensions': {
                'os': 'Linux'
            },
        }
    },
    'foo_test': {
        'name': 'foo_test',
        'swarming': {
            'dimensions': {
                'os': 'Linux'
            },
        }
    }
}

TEST_QUERY_TESTS_MULTIPLE_PARAMS_OUTPUT = ['foo_test']

TEST_QUERY_TESTS_DIMENSION_PARAMS_OUTPUT = ['bar_test']

TEST_QUERY_TESTS_SWARMING_PARAMS_OUTPUT = ['bar_test_test']

TEST_QUERY_TESTS_PARAMS_OUTPUT = ['bar_test_test']

TEST_QUERY_TESTS_PARAMS_FALSE_OUTPUT = ['bar_test']

TEST_QUERY_TEST_OUTPUT = {
    'name': 'foo_test',
    'swarming': {
        'dimensions': {
            'os': 'Linux',
        },
    },
}

TEST_QUERY_TEST_BOTS_OUTPUT = [
    'Fake Android K Tester',
    'Fake Android L Tester',
    'Fake Android M Tester',
]

TEST_QUERY_TEST_BOTS_ISOLATED_SCRIPTS_OUTPUT = ['Fake Tester']

TEST_QUERY_TEST_BOTS_NO_BOTS_OUTPUT = []


class QueryTests(TestCase):
  """Tests for the query feature."""
  def test_query_bots(self):
    self.set_args('--query=bots')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_BOTS_OUTPUT)

  def test_query_bots_invalid(self):
    self.set_args('--query=bots/blah/blah')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_bots_json(self):
    self.set_args('--query=bots', '--json=result.json')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    self.assertFalse(fbb.printed_lines)

  def test_query_bots_tests(self):
    self.set_args('--query=bots/tests')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_BOTS_TESTS_OUTPUT)

  def test_query_invalid_bots_tests(self):
    self.set_args('--query=bots/tdfjdk')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_bot(self):
    self.set_args('--query=bot/Fake Android K Tester')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.maxDiff = None  # pragma pylint: disable=attribute-defined-outside-init
    self.assertEqual(query_json, TEST_QUERY_BOT_OUTPUT)

  def test_query_bot_invalid_id(self):
    self.set_args('--query=bot/bot1')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_bot_invalid_query_too_many(self):
    self.set_args('--query=bot/Fake Android K Tester/blah/blah')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_bot_invalid_query_no_tests(self):
    self.set_args('--query=bot/Fake Android K Tester/blahs')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_bot_tests(self):
    self.set_args('--query=bot/Fake Android L Tester/tests')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_BOT_TESTS_OUTPUT)

  def test_query_tests(self):
    self.set_args('--query=tests')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_OUTPUT)

  def test_query_tests_invalid(self):
    self.set_args('--query=tests/blah/blah')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_tests_multiple_params(self):
    self.set_args('--query=tests/--jobs=1&--verbose')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_MULTIPLE_PARAMS_OUTPUT)

  def test_query_tests_invalid_params(self):
    self.set_args('--query=tests/device_os?')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_tests_dimension_params(self):
    self.set_args('--query=tests/device_os:NMF26U')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_DIMENSION_PARAMS_OUTPUT)

  def test_query_tests_swarming_params(self):
    self.set_args('--query=tests/hard_timeout:1000')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_SWARMING_PARAMS_OUTPUT)

  def test_query_tests_params(self):
    self.set_args('--query=tests/should_retry_with_patch:true')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_PARAMS_OUTPUT)

  def test_query_tests_params_false(self):
    self.set_args('--query=tests/should_retry_with_patch:false')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    TEST_SUITE_WITH_PARAMS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TESTS_PARAMS_FALSE_OUTPUT)

  def test_query_test(self):
    self.set_args('--query=test/foo_test')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TEST_OUTPUT)

  def test_query_test_invalid_id(self):
    self.set_args('--query=test/foo_foo')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_test_invalid_length(self):
    self.set_args('--query=test/foo_tests/foo/foo')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_test_bots(self):
    self.set_args('--query=test/foo_test/bots')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TEST_BOTS_OUTPUT)

  def test_query_test_bots_isolated_scripts(self):
    self.set_args('--query=test/foo_test/bots')
    fbb = FakeBBGen(self.args,
                    FOO_ISOLATED_SCRIPTS_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TEST_BOTS_ISOLATED_SCRIPTS_OUTPUT)

  def test_query_test_bots_invalid(self):
    self.set_args('--query=test/foo_tests/foo')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)

  def test_query_test_bots_no_bots(self):
    self.set_args('--query=test/bar_tests/bots')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    fbb.query(fbb.args)
    query_json = json.loads(''.join(fbb.printed_lines))
    self.assertEqual(query_json, TEST_QUERY_TEST_BOTS_NO_BOTS_OUTPUT)

  def test_query_invalid(self):
    self.set_args('--query=foo')
    fbb = FakeBBGen(self.args,
                    ANDROID_WATERFALL,
                    GOOD_COMPOSITION_TEST_SUITES,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS_SORTED)
    with self.assertRaises(SystemExit) as cm:
      fbb.query(fbb.args)
      self.assertEqual(cm.exception.code, 1)
    self.assertTrue(fbb.printed_lines)


FOO_TEST_SUITE_WITH_ENABLE_FEATURES_SEPARATE_ENTRIES = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'args': [
          '--enable-features',
          'Foo,Bar',
        ],
      },
    },
  },
}
"""


FOO_TEST_REPLACEMENTS_REMOVE_NO_VALUE = """\
{
  'foo_test': {
    'replacements': {
      'Fake Tester': {
        'args': {
          '--c_arg': None,
        },
      },
    },
  },
}
"""


FOO_TEST_REPLACEMENTS_REMOVE_VALUE = """\
{
  'foo_test': {
    'replacements': {
      'Fake Tester': {
        'args': {
          '--enable-features': None,
        },
      },
    },
  },
}
"""


FOO_TEST_REPLACEMENTS_REPLACE_VALUE = """\
{
  'foo_test': {
    'replacements': {
      'Fake Tester': {
        'args': {
          '--enable-features': 'Bar,Baz',
        },
      },
    },
  },
}
"""


FOO_TEST_REPLACEMENTS_INVALID_KEY = """\
{
  'foo_test': {
    'replacements': {
      'Fake Tester': {
        'invalid': {
          '--enable-features': 'Bar,Baz',
        },
      },
    },
  },
}
"""


class ReplacementTests(TestCase):
  """Tests for the arg replacement feature."""
  def test_replacement_valid_remove_no_value(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_REMOVE_NO_VALUE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_replacement_valid_remove_value(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ENABLE_FEATURES,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_REMOVE_VALUE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_replacement_valid_replace_value(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ENABLE_FEATURES,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_REPLACE_VALUE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_replacement_valid_replace_value_separate_entries(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ENABLE_FEATURES_SEPARATE_ENTRIES,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_REPLACE_VALUE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_replacement_invalid_key_not_valid(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_INVALID_KEY)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Given replacement key *'):
      fbb.check_output_file_consistency(verbose=True)

  def test_replacement_invalid_key_not_found(self):
    fbb = FakeBBGen(self.args,
                    FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_ARGS,
                    LUCI_MILO_CFG,
                    exceptions=FOO_TEST_REPLACEMENTS_REPLACE_VALUE)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Could not find *'):
      fbb.check_output_file_consistency(verbose=True)


FOO_TEST_SUITE_WITH_MAGIC_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'args': [
          '$$MAGIC_SUBSTITUTION_TestOnlySubstitution',
        ],
      },
    },
  },
}
"""


FOO_TEST_SUITE_WITH_INVALID_MAGIC_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
        'args': [
          '$$MAGIC_SUBSTITUTION_NotARealSubstitution',
        ],
      },
    },
  },
}
"""


class MagicSubstitutionTests(TestCase):
  """Tests for the magic substitution feature."""
  def test_valid_function(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_MAGIC_ARGS, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_invalid_function(self):
    fbb = FakeBBGen(self.args, FOO_GTESTS_WATERFALL,
                    FOO_TEST_SUITE_WITH_INVALID_MAGIC_ARGS, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Magic substitution function *'):
      fbb.check_output_file_consistency(verbose=True)


# Matrix compound composition test suites

MATRIX_COMPOUND_EMPTY = """\
{
  'basic_suites': {
    'bar_tests': {
      'bar_test': {},
    },
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {},
      'bar_tests': {},
    },
  },
}
"""

MATRIX_COMPOUND_EMPTY_WITH_DESCRIPTION = """\
{
  'basic_suites': {
    'bar_tests': {
      'bar_test': {
        'description': 'This is a bar test',
      },
    },
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {},
      'bar_tests': {},
    },
  },
}
"""

MATRIX_COMPOUND_MISSING_IDENTIFIER = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'missing-identifier',
        ],
      },
    },
  },
}
"""

VARIANTS_FILE_MISSING_IDENTIFIER = """\
{
  'missing-identifier': {}
}
"""

MATRIX_COMPOUND_EMPTY_IDENTIFIER = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'empty-identifier',
        ],
      },
    },
  },
}
"""

EMPTY_IDENTIFIER_VARIANTS = """\
{
  'empty-identifier': {
    'identifier': '',
    'swarming': {
      'dimensions': {
        'foo': 'empty identifier not allowed',
      },
    },
  },
}
"""

MATRIX_COMPOUND_TRAILING_IDENTIFIER = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'trailing-whitespace',
        ],
      },
    },
  },
}
"""

TRAILING_WHITESPACE_VARIANTS = """\
{
  'trailing-whitespace': {
    'identifier': 'id ',
    'swarming': {
      'dimensions': {
        'foo': 'trailing whitespace not allowed',
      },
    },
  },
}
"""

MATRIX_REF_NONEXISTENT = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'bar_test': {},
    },
  },
}
"""

MATRIX_COMPOUND_REF_COMPOSITION = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'compound_suites': {
    'sample_composition': {
      'foo_tests': {},
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'sample_composition': {},
    },
  },
}
"""

MATRIX_COMPOSITION_REF_MATRIX = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {},
    },
  },
  'matrix_compound_suites': {
    'a_test': {
      'foo_tests': {},
    },
    'matrix_tests': {
      'a_test': {},
    },
  },
}
"""

MATRIX_COMPOUND_VARIANTS_MIXINS_MERGE = """\
{
  'basic_suites': {
    'foo_tests': {
      'set': {
        'mixins': [ 'test_mixin' ],
      },
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          {
            'mixins': [ 'dimension_mixin' ],
          },
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_VARIANTS_MIXINS = """\
{
  'basic_suites': {
    'foo_tests': {
      'set': {
        'mixins': [ 'test_mixin' ],
      },
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          {
            'mixins': [
              'dimension_mixin',
              'waterfall_mixin',
              'builder_mixin',
              'random_mixin'
            ],
          },
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_VARIANTS_MIXINS_REMOVE = """\
{
  'basic_suites': {
    'foo_tests': {
      'set': {
        'remove_mixins': ['builder_mixin'],
      },
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          {
            'mixins': [ 'builder_mixin' ],
          }
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_CONFLICTING_TEST_SUITES = """\
{
  'basic_suites': {
    'bar_tests': {
      'baz_tests': {
        'args': [
          '--bar',
        ],
      }
    },
    'foo_tests': {
      'baz_tests': {
        'args': [
          '--foo',
        ],
      }
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'bar_tests': {
        'variants': [
          'a_variant',
        ],
      },
      'foo_tests': {
        'variants': [
          'a_variant',
        ]
      }
    },
  },
}
"""

MATRIX_COMPOUND_EMPTY_VARIANTS = """\
{
  'basic_suites': {
    'foo_tests': {
      'baz_tests': {
        'args': [
          '--foo',
        ],
      }
    },
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [],
      }
    },
  },
}
"""

MATRIX_COMPOUND_TARGETS_ARGS = """\
{
  'basic_suites': {
    'foo_tests': {
      'args_test': {
        'args': [
          '--iam'
        ],
      },
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'an-arg',
        ],
      },
    },
  },
}
"""

ARGS_VARIANTS_FILE = """\
{
  'an-arg': {
    'identifier': 'args',
    'args': [
      '--anarg',
    ],
  },
}
"""

MATRIX_COMPOUND_TARGETS_MIXINS = """\
{
  'basic_suites': {
    'foo_tests': {
      'mixins_test': {
        'mixins': [ 'test_mixin' ],
      },
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'mixins': [ 'random_mixin' ],
        'variants': [
          'mixins',
        ],
      },
    },
  },
}
"""

MIXINS_VARIANTS_FILE = """\
{
  'mixins': {
    'identifier': 'mixins',
    'mixins': [ 'dimension_mixin' ],
  },
}
"""

MATRIX_COMPOUND_TARGETS_SWARMING = """\
{
  'basic_suites': {
    'foo_tests': {
      'swarming_test': {
        'swarming': {
          'foo': 'bar',
          'dimensions': {
            'foo': 'bar',
          },
        },
      },
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'swarming-variant',
        ],
      },
    },
  },
}
"""

SWARMING_VARIANTS_FILE = """\
{
  'swarming-variant': {
    'identifier': 'swarming',
    'swarming': {
      'a': 'b',
      'dimensions': {
        'hello': 'world',
      },
    },
  },
}
"""

MATRIX_COMPOUND_VARIANTS_REF = """\
{
  'basic_suites': {
    'foo_tests': {
      'swarming_test': {},
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'a_variant'
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_VARIANTS_REF_WITH_DESCRIPTION = """\
{
  'basic_suites': {
    'foo_tests': {
      'swarming_test': {
        'description': 'This is a swarming test.'
      },
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'a_variant'
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_TEST_WITH_TEST_KEY = """\
{
  'basic_suites': {
    'foo_tests': {
      'swarming_test': {
          'test': 'foo_test_apk'
      },
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'a_variant',
        ],
      },
    },
  },
}
"""

MATRIX_COMPOUND_MIXED_VARIANTS_REF = """\
{
  'basic_suites': {
    'foo_tests': {
      'swarming_test': {},
    }
  },
  'matrix_compound_suites': {
    'matrix_tests': {
      'foo_tests': {
        'variants': [
          'a_variant',
          'b_variant',
        ],
      },
    },
  },
}
"""

VARIANTS_FILE = """\
{
  'a_variant': {
    'args': [
      '--platform',
      'device',
      '--version',
      '1'
    ],
    'identifier': 'a_variant'
  }
}
"""

VARIANTS_FILE_WITH_DESCRIPTION = """\
{
  'a_variant': {
    'identifier': 'a_variant',
    'description': 'Variant description.'
  }
}
"""

MULTI_VARIANTS_FILE = """\
{
  'a_variant': {
    'args': [
      '--platform',
      'device',
      '--version',
      '1'
    ],
    'identifier': 'a_variant'
  },
  'b_variant': {
    'args': [
      '--platform',
      'sim',
      '--version',
      '2'
    ],
    'identifier': 'b_variant'
  }
}
"""

# # Dictionary composition test suite outputs

EMPTY_SKYLAB_TEST_EXCEPTIONS = """\
{
  'tast.foo OCTOPUS_TOT': {
    'remove_from': [
      'Fake Tester',
    ]
  },
  'tast.foo OCTOPUS_TOT-1': {
    'remove_from': [
      'Fake Tester',
    ]
  }
}
"""

MATRIX_SKYLAB_WATERFALL_WITH_NO_CROS_BOARD = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'skylab_tests': 'cros_skylab_basic_x86',
        },
      },
    },
  },
]
"""

MATRIX_SKYLAB_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'skylab_tests': 'cros_skylab_basic_x86',
        },
        'cros_board': 'octopus',
        'cros_dut_pool': 'chromium',
        'run_cft': True,
      },
    },
  },
]
"""

MATRIX_SKYLAB_WATERFALL_WITH_BUILD_TARGET_VARIANT = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Fake Tester': {
        'test_suites': {
          'skylab_tests': 'cros_skylab_basic_x86',
        },
        'cros_board': 'octopus',
        'cros_build_target': 'octopus-arc-t',
        'cros_dut_pool': 'chromium',
        'run_cft': True,
      },
    },
  },
]
"""

MATRIX_COMPOUND_SKYLAB_REF = """\
{
  'basic_suites': {
    'autotest_suites': {
      'autotest_suite': {
      },
    },
    'cros_skylab_basic': {
      'benchmark_suite': {
        'benchmark': 'something',
      },
      'chrome_all_tast_tests': {
        'tast_expr': 'dummy expr',
        'timeout': 3600,
      },
      'gtest_suite': { },
      'lacros_all_tast_tests': {
        'tast_expr': 'lacros expr',
        'timeout': 3600,
      },
    },
  },
  'compound_suites': {},
  'matrix_compound_suites': {
    'cros_skylab_basic_x86': {
      'autotest_suites': {
        'variants': [
          'octopus-89-with-autotest-name',
        ],
      },
      'cros_skylab_basic': {
        'tast_expr': 'dummy expr',
        'variants': [
          'octopus-89',
          'octopus-88',
        ],
      },
    },
  },
}
"""

SKYLAB_VARIANTS = """\
{
  'octopus-89': {
    'skylab': {
      'cros_board': 'octopus',
      'cros_model': 'casta',
      'cros_chrome_version': '89.0.3234.0',
      'cros_img': 'octopus-release/R89-13655.0.0',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT',
  },
  'octopus-89-with-autotest-name': {
    'skylab': {
      'cros_chrome_version': '89.0.3234.0',
      'cros_img': 'octopus-release/R89-13655.0.0',
      'autotest_name': 'unique_autotest_name',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT',
  },
  'octopus-88': {
    'skylab': {
      'cros_chrome_version': '88.0.2324.0',
      'cros_img': 'octopus-release/R88-13597.23.0',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT-1',
  },
}
"""

SKYLAB_VARIANTS_WITH_BUILD_VARIANT = """\
{
  'octopus-89': {
    'skylab': {
      'cros_board': 'octopus',
      'cros_model': 'casta',
      'cros_chrome_version': '89.0.3234.0',
      'cros_img': 'octopus-arc-t-release/R89-13655.0.0',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT',
  },
  'octopus-89-with-autotest-name': {
    'skylab': {
      'cros_chrome_version': '89.0.3234.0',
      'cros_img': 'octopus-arc-t-release/R89-13655.0.0',
      'autotest_name': 'unique_autotest_name',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT',
  },
  'octopus-88': {
    'skylab': {
      'cros_chrome_version': '88.0.2324.0',
      'cros_img': 'octopus-arc-t-release/R88-13597.23.0',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT-1',
  },
}
"""

ENABLED_AND_DISABLED_MATRIX_COMPOUND_SKYLAB_REF = """\
{
  'basic_suites': {
    'cros_skylab_basic': {
      'tast.basic': {
        'tast_expr': 'dummy expr',
        'suite': 'tast.basic',
        'shard_level_retries_on_ctp': 2,
        'timeout': 3600,
      },
      'tast.foo': {
        'tast_expr': 'dummy expr',
        'suite': 'tast.foo',
        'timeout': 3600,
      },
    },
  },
  'compound_suites': {},
  'matrix_compound_suites': {
    'cros_skylab_basic_x86': {
      'cros_skylab_basic': {
        'variants': [
          'enabled',
          'disabled',
        ],
      },
    },
  },
}
"""

ENABLED_AND_DISABLED_VARIANTS = """\
{
  'enabled': {
    'skylab': {
      'cros_board': 'octopus',
      'cros_chrome_version': '89.0.3234.0',
      'cros_img': 'octopus-release/R89-13655.0.0',
    },
    'enabled': True,
    'identifier': 'OCTOPUS_TOT',
  },
  'disabled': {
    'skylab': {
      'cros_board': 'octopus',
      'cros_chrome_version': '88.0.2324.0',
      'cros_img': 'octopus-release/R88-13597.23.0',
    },
    'enabled': False,
    'identifier': 'OCTOPUS_TOT-1',
  },
}
"""


class MatrixCompositionTests(TestCase):

  def test_good_structure_no_configs(self):
    """
    Tests matrix compound test suite structure with no configs,
    no conflicts and no bad references
    """
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_EMPTY, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_good_structure_with_description(self):
    """
    Tests matrix compound test suite structure with description.
    """
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_EMPTY_WITH_DESCRIPTION, LUCI_MILO_CFG)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_missing_identifier(self):
    """
    Variant is missing an identifier
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_MISSING_IDENTIFIER,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE_MISSING_IDENTIFIER)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Missing required identifier field in matrix compound suite*'):
      fbb.check_output_file_consistency(verbose=True)

  def test_empty_identifier(self):
    """
    Variant identifier is empty.
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_EMPTY_IDENTIFIER,
                    LUCI_MILO_CFG,
                    variants=EMPTY_IDENTIFIER_VARIANTS)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Identifier field can not be "" in matrix compound suite*'):
      fbb.check_output_file_consistency(verbose=True)

  def test_trailing_identifier(self):
    """
    Variant identifier has trailing whitespace.
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TRAILING_IDENTIFIER,
                    LUCI_MILO_CFG,
                    variants=TRAILING_WHITESPACE_VARIANTS)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'Identifier field can not have leading and trailing whitespace in'
        ' matrix compound suite*'):
      fbb.check_output_file_consistency(verbose=True)

  def test_noexistent_ref(self):
    """
    Test referencing a non-existent basic test suite
    """
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_REF_NONEXISTENT, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Unable to find reference to *'):
      fbb.check_output_file_consistency(verbose=True)

  def test_ref_to_composition(self):
    """
    Test referencing another composition test suite
    """
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_REF_COMPOSITION, LUCI_MILO_CFG)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'matrix_compound_suites may not refer to other *'):
      fbb.check_output_file_consistency(verbose=True)

  def test_ref_to_matrix(self):
    """
    Test referencing another matrix test suite
    """
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOSITION_REF_MATRIX, LUCI_MILO_CFG)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'matrix_compound_suites may not refer to other *'):
      fbb.check_output_file_consistency(verbose=True)

  def test_conflicting_names(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_CONFLICTING_TEST_SUITES,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Conflicting test definitions.*'):
      fbb.check_input_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_empty_variants(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_EMPTY_VARIANTS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_swarming_dict_merge_args(self):
    """
    Test targets with swarming dictionary defined by both basic and matrix
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TARGETS_ARGS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS,
                    variants=ARGS_VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_swarming_dict_merge_mixins(self):
    """
    Test targets with swarming dictionary defined by both basic and matrix
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TARGETS_MIXINS,
                    LUCI_MILO_CFG,
                    mixins=SWARMING_MIXINS,
                    variants=MIXINS_VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_swarming_dict_swarming(self):
    """
    Test targets with swarming dictionary defined by both basic and matrix
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TARGETS_SWARMING,
                    LUCI_MILO_CFG,
                    variants=SWARMING_VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variant_test_suite_with_test_key(self):
    """
    Test targets in matrix compound test suites with variants
    """
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_TEST_WITH_TEST_KEY,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_with_description(self):
    """Test variants with description field"""
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_VARIANTS_REF,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE_WITH_DESCRIPTION)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_both_test_suite_and_variants_with_description(self):
    """Test both test suite and variants with description field"""
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_VARIANTS_REF_WITH_DESCRIPTION,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE_WITH_DESCRIPTION)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_pyl_ref(self):
    """Test targets with variants string ref"""
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_VARIANTS_REF,
                    LUCI_MILO_CFG,
                    variants=VARIANTS_FILE)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_variants_pyl_no_ref(self):
    """Test targets with variants string ref, not defined in variants.pyl"""
    fbb = FakeBBGen(self.args, MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_VARIANTS_REF, LUCI_MILO_CFG)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'Missing variant definition for *'):
      fbb.check_output_file_consistency(verbose=True)

  def test_variants_pyl_all_unreferenced(self):
    """Test targets with variants in variants.pyl, unreferenced in tests"""
    fbb = FakeBBGen(self.args,
                    MATRIX_GTEST_SUITE_WATERFALL,
                    MATRIX_COMPOUND_VARIANTS_REF,
                    LUCI_MILO_CFG,
                    variants=MULTI_VARIANTS_FILE)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'The following variants were unreferenced *'):
      fbb.check_input_file_consistency(verbose=True)

  def test_good_skylab_matrix_with_variants(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_SKYLAB_WATERFALL,
                    MATRIX_COMPOUND_SKYLAB_REF,
                    LUCI_MILO_CFG,
                    exceptions=EMPTY_SKYLAB_TEST_EXCEPTIONS,
                    variants=SKYLAB_VARIANTS)
    fbb.check_input_file_consistency(verbose=True)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_good_skylab_matrix_with_build_target_variant_and_variants(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_SKYLAB_WATERFALL_WITH_BUILD_TARGET_VARIANT,
                    MATRIX_COMPOUND_SKYLAB_REF,
                    LUCI_MILO_CFG,
                    exceptions=EMPTY_SKYLAB_TEST_EXCEPTIONS,
                    variants=SKYLAB_VARIANTS_WITH_BUILD_VARIANT)
    fbb.check_input_file_consistency(verbose=True)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_enabled_and_disabled_skylab_matrix_with_variants(self):
    """Test with disabled variants"""
    fbb = FakeBBGen(self.args,
                    MATRIX_SKYLAB_WATERFALL,
                    ENABLED_AND_DISABLED_MATRIX_COMPOUND_SKYLAB_REF,
                    LUCI_MILO_CFG,
                    exceptions=EMPTY_SKYLAB_TEST_EXCEPTIONS,
                    variants=ENABLED_AND_DISABLED_VARIANTS)
    # some skylab test variant is disabled; the corresponding skylab tests
    # is not generated.
    fbb.check_input_file_consistency(verbose=True)
    fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_invalid_skylab_matrix_with_variants(self):
    fbb = FakeBBGen(self.args,
                    MATRIX_SKYLAB_WATERFALL_WITH_NO_CROS_BOARD,
                    MATRIX_COMPOUND_SKYLAB_REF,
                    LUCI_MILO_CFG,
                    exceptions=EMPTY_SKYLAB_TEST_EXCEPTIONS,
                    variants=SKYLAB_VARIANTS)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'skylab tests must specify cros_board.'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)


MAC_TEST_SUITE = """\
{
  'basic_suites': {
    'foo_tests': {
      'foo_test': {
      },
    },
  },
}
"""

NO_DIMENSIONS_GTESTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Mac': {
        'swarming': {},
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

NO_OS_GTESTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Mac': {
        'swarming': {
          'dimensions': {
            'foo': 'bar',
          },
        },
        'test_suites': {
          'gtest_tests': 'foo_tests',
        },
      },
    },
  },
]
"""

MAC_ISOLATED_SCRIPTS_WATERFALL = """\
[
  {
    'project': 'chromium',
    'bucket': 'ci',
    'name': 'chromium.test',
    'machines': {
      'Mac': {
        'swarming': {
          'dimensions': {
            'os': 'Mac',
          },
        },
        'test_suites': {
          'isolated_scripts': 'foo_tests',
        },
      },
    },
  },
]
"""

MAC_LUCI_MILO_CFG = """\
consoles {
  builders {
    name: "buildbucket/luci.chromium.ci/Mac"
  }
}
"""


class SwarmingTests(TestCase):
  def test_builder_with_no_dimension_fails(self):
    fbb = FakeBBGen(self.args, NO_DIMENSIONS_GTESTS_WATERFALL, MAC_TEST_SUITE,
                    MAC_LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(
        generate_buildbot_json.BBGenErr,
        'dimensions must be specified for all swarmed tests'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_builder_with_no_os_dimension_fails(self):
    fbb = FakeBBGen(self.args, NO_OS_GTESTS_WATERFALL, MAC_TEST_SUITE,
                    MAC_LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'os must be specified for all swarmed tests'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)

  def test_mac_builder_with_no_cpu_dimension_in_isolated_script_fails(self):
    fbb = FakeBBGen(self.args, MAC_ISOLATED_SCRIPTS_WATERFALL, MAC_TEST_SUITE,
                    MAC_LUCI_MILO_CFG)
    fbb.check_input_file_consistency(verbose=True)
    with self.assertRaisesRegex(generate_buildbot_json.BBGenErr,
                                'cpu must be specified for mac'):
      fbb.check_output_file_consistency(verbose=True)
    self.assertFalse(fbb.printed_lines)


if __name__ == '__main__':
  unittest.main()