blob: 55b3f0fb47e4a0f3c44e7d109e39a4f7efed3684 [file] [log] [blame] [edit]
"""Tests for tests.protoascii configs."""
import abc
import collections
import itertools
import os
import re
import unittest
from catatester.genfiles import continuous_tests_pb2
from catatester import config_utils
# Import from eureka-internal/builder/masters project.
# pylint: disable=ungrouped-imports
try:
from src.genfiles import build_config_pb2
except ImportError:
from catatester.genfiles import build_config_pb2
import google.protobuf.text_format
_CATATESTER_ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
_CONTINUOUS_TESTS_ROOT_PATH = os.path.dirname(_CATATESTER_ROOT_PATH)
_SKIP_UNIT_TEST_RE = re.compile(r'unit_test.*')
_REQUIRED_FIELDS = ['TEST_CASE_CLASS', 'TEST_CASE_MODULE',
'TEST_SEQUENCE_CLASS', 'TEST_SEQUENCE_MODULE']
# Cap the execution_timeout to 23 hours (b/176174164)
_MAX_EXECUTION_TIMEOUT_MINS = 23 * 60
# Assumes full test manifest repo checkout.
_BUILD_CONFIG_FILEPATH = os.path.join(
os.path.dirname(_CONTINUOUS_TESTS_ROOT_PATH), 'builder', 'masters',
'configs', 'eureka', 'build_config.protoascii')
def _GetBuildConfig():
"""Returns the build_config protoascii from the builder/masters project.
Returns:
Ingested build_config.
"""
build_config = build_config_pb2.BuildConfig()
with open(_BUILD_CONFIG_FILEPATH) as f:
google.protobuf.text_format.Merge(f.read(), build_config)
return build_config
# pylint: disable=no-member
# Test is not directly derived from UnitTest base class.
class ConfigTest(object):
"""A class to test a continuous_tests config.
Note: This class will be tested twice, once with the staging config
and once with the prod config.
"""
ASSISTANT = build_config_pb2.OtaBuild.Capability.Value(
'ASSISTANT')
CAST_VIDEO_RECEIVER = build_config_pb2.OtaBuild.Capability.Value(
'CAST_VIDEO_RECEIVER')
CAST_AUDIO_RECEIVER = build_config_pb2.OtaBuild.Capability.Value(
'CAST_AUDIO_RECEIVER')
def __init__(self):
self.__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def tests_proto(self):
pass
@property
def build_config_proto(self):
return _GetBuildConfig()
@property
def all_tests(self):
return (
list(self.tests_proto.continuous_test) +
list(self.tests_proto.scheduled_test))
def testContinuousTestsConfig_IsValidProto(self):
"""Tests the continuous_tests config is a valid proto."""
self.assertTrue(self.tests_proto)
def testEveryTestTarget_hasTestConfigOrTestBinary(self):
"""Test that every test_target has a test_config or test_binary_name."""
for test in self.all_tests:
self.assertTrue(
test.test_target.binary_name or
test.test_target.config)
def testEveryScheduledTestsBuildTargetsHasProductsOrProductTypes(self):
"""Test that scheduled test build_target has products or product types."""
for test in self.tests_proto.scheduled_test:
for target in test.build_targets:
msg = ('Scheduled tests require build_target.products or'
'build_target.product_types: %s' % test)
self.assertTrue(
target.products or
target.product_types, msg)
def testEveryScheduledTestHasSetACron(self):
"""Verify every scheduled test has set a cron string."""
for test in self.tests_proto.scheduled_test:
self.assertTrue(test.schedule.cron,
'scheduled_test is missing cron string: %s' % test)
def testEveryTestTargetGroupIsDefined(self):
"""Test that all test target groups are defined in test_group_tags."""
for test in self.all_tests:
for test_group in test.test_target.test_groups:
self.assertIn(test_group, self.tests_proto.test_group_tags)
def testEveryTestContainsGroup(self):
"""Ensures group is included in every test."""
for test in self.all_tests:
groups = [dim for dim in test.test_target.testbed_requirements if
dim.bot_dimension.lower() == 'group']
self.assertTrue(groups,
'Missing group in target: %s' % test.test_target.config)
def testEveryTestHasValidTestTimeout(self):
"""Ensures test_timeout_mins is under the max allowed value."""
for test in self.all_tests:
self.assertLessEqual(
test.test_target.test_timeout_mins,
_MAX_EXECUTION_TIMEOUT_MINS,
'Timeout mins > %s in target: %s' % (_MAX_EXECUTION_TIMEOUT_MINS,
test.test_target.config))
def testEveryTargetedTestConfigFileExists(self):
"""Test that every targeted test_config file exists."""
catatester_path = os.path.abspath(os.path.dirname(__file__))
root_path = os.path.dirname(catatester_path)
test_configs_root = os.path.join(
root_path, self.tests_proto.test_configs_root)
for test in self.all_tests:
if test.test_target.config:
config_abspath = os.path.join(
test_configs_root,
test.test_target.config)
self.assertTrue(
os.path.isfile(config_abspath),
'test_target.config does not exist: %s' % config_abspath)
def testEveryTestConfigFileContainsRequiredFields(self):
"""Test that every test_config file contains required fields."""
for test in self.all_tests:
if (not test.test_target.config
or re.match(_SKIP_UNIT_TEST_RE, test.test_target.config)):
continue
fname = os.path.join(_CONTINUOUS_TESTS_ROOT_PATH,
self.tests_proto.test_configs_root, test.test_target.config)
with open(fname) as f:
lines = f.readlines()
found_fields = []
for field in _REQUIRED_FIELDS:
for line in lines:
if not re.match('^%s.*' % field, line, re.I):
continue
_, value = re.split('=', line, 2)
self.assertIsNotNone( value.strip(),
'%s in %s must have a value.' % (field, fname))
found_fields.append(field)
self.assertEqual(_REQUIRED_FIELDS, sorted(found_fields),
'%s must contain all of %s.' % (fname, _REQUIRED_FIELDS))
def testTarget_ValidCipdRefsKey(self):
"""Tests targets provide valid cipd refs."""
cipd_ref_keys = self.tests_proto.cipd_refs.keys()
for test in self.all_tests:
msg = 'cipd_ref_key should be one of %s but got %s' % (
cipd_ref_keys, test.test_target.cipd_refs_key)
self.assertIn(test.test_target.cipd_refs_key, cipd_ref_keys, msg)
def testContinuousTestsConfigDuplicates(self):
"""Tests there are no duplicated definitions for continuous_tests."""
dict_continuous_tests = collections.defaultdict(list)
for continuous_test in self.tests_proto.continuous_test:
products_set = self._GetProductsForContinuousTest(continuous_test)
config_variants = list(continuous_test.build_targets)[0].variants
testbed_reqs = self._GetTestbedRequirementsForContinuousTest(
continuous_test)
config_key = '%s-%s-%s' % (
continuous_test.test_target.config,
continuous_tests_pb2.BuildTargets.Variant.Name(config_variants),
';'.join(testbed_reqs))
if not continuous_test.test_target.test_cases:
dict_continuous_tests[config_key].append(products_set)
else:
for test_case in continuous_test.test_target.test_cases:
config_test_case_key = '%s-%s' % (config_key, test_case)
dict_continuous_tests[config_test_case_key].append(products_set)
for config in dict_continuous_tests:
combinations = itertools.combinations(dict_continuous_tests[config], 2)
duplicates = next((c for c in combinations if set.intersection(*c)), None)
msg = 'Overlapping config found for %s, products %s' % (config,
duplicates)
self.assertIsNone(duplicates, msg)
def _GetDevicesByCapability(self, capability_index):
"""Getting sets of devices filtered by the index received.
Args:
capability_index: Number provided to filter the capabilities of devices
Returns:
returns the set of filtered devices
For example, if capability_index is CAST_VIDEO_RECEIVER:
{'chorizo', 'steak', 'salami'}
"""
ota_dict = self.build_config_proto.ota_build
set_cast_products = set()
for key in ota_dict:
if ota_dict[key].capabilities:
if capability_index in list(ota_dict[key].capabilities):
set_cast_products.add(ota_dict[key].product)
return set_cast_products
def _GetProductsForContinuousTest(self, continuous_test):
"""Getting set of products for the specified continuous_test.
Args:
continuous_test: continuous test object.
Returns:
returns a set with products of the continuous test
"""
assistant_devices = self._GetDevicesByCapability(self.ASSISTANT)
cast_video_receiver = self._GetDevicesByCapability(
self.CAST_VIDEO_RECEIVER)
cast_audio_receiver = self._GetDevicesByCapability(
self.CAST_AUDIO_RECEIVER)
set_products = set()
set_product_types = set()
for build_target in continuous_test.build_targets:
if build_target.products:
set_products.update(list(build_target.products))
if build_target.product_types:
product_types = list(build_target.product_types)
if 'ASSISTANT' in product_types:
set_product_types |= assistant_devices
if 'CAST_AUDIO_RECEIVER' in product_types:
set_product_types |= cast_audio_receiver
if 'CAST_VIDEO_RECEIVER' in product_types:
set_product_types |= cast_video_receiver
products = set_products | set_product_types
return products
def _GetTestbedRequirementsForContinuousTest(self, continuous_test):
"""Returns a list of bot dimensions required for the test."""
test_target = continuous_test.test_target
if not test_target.skip_default_dimensions:
return ['dims:default']
requirements = []
for req in test_target.testbed_requirements:
requirements.append(':'.join([req.bot_dimension, req.value]))
return requirements
def testEveryTestTargetBinaryNameExists(self):
"""Test that every test_target.binary_name does exist as subdir.
Except for test_target with recipe as recipe defines subdir.
"""
binaries_and_recipes = {(test.test_target.binary_name,
test.test_target.recipe)
for test in self.all_tests
if test.test_target.binary_name}
for binary_name, recipe in binaries_and_recipes:
subdir = os.path.join(_CONTINUOUS_TESTS_ROOT_PATH,
'lab_system', 'master', 'config', 'tests',
binary_name)
subdir = os.path.abspath(subdir)
self.assertTrue(os.path.isdir(subdir) or recipe,
msg='%s does not exist.' % subdir)
class ProdConfigTest(unittest.TestCase, ConfigTest):
@property
def tests_proto(self):
return config_utils.parse_continuous_tests_protoascii(
config_utils.PROD_CONFIG_PATH)
@property
def build_restrictions(self):
return self.tests_proto.build_restrictions
class StagingConfigTest(unittest.TestCase, ConfigTest):
@property
def tests_proto(self):
return config_utils.parse_continuous_tests_protoascii(
config_utils.STAGING_CONFIG_PATH)
if __name__ == '__main__':
unittest.main()