1# Copyright 2022 The gRPC Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""The classes and predicates to assist validate test config for test cases.""" 15from dataclasses import dataclass 16import enum 17import logging 18import re 19from typing import Callable, Optional 20import unittest 21 22from packaging import version as pkg_version 23 24from framework import xds_flags 25from framework import xds_k8s_flags 26 27logger = logging.getLogger(__name__) 28 29 30class Lang(enum.Flag): 31 UNKNOWN = enum.auto() 32 CPP = enum.auto() 33 GO = enum.auto() 34 JAVA = enum.auto() 35 PYTHON = enum.auto() 36 NODE = enum.auto() 37 38 def __str__(self): 39 return str(self.name).lower() 40 41 @classmethod 42 def from_string(cls, lang: str): 43 try: 44 return cls[lang.upper()] 45 except KeyError: 46 return cls.UNKNOWN 47 48 49@dataclass 50class TestConfig: 51 """Describes the config for the test suite. 52 53 TODO(sergiitk): rename to LangSpec and rename skips.py to lang.py. 54 """ 55 client_lang: Lang 56 server_lang: Lang 57 version: Optional[str] 58 59 def version_gte(self, another: str) -> bool: 60 """Returns a bool for whether this VERSION is >= then ANOTHER version. 61 62 Special cases: 63 64 1) Versions "master" or "dev" are always greater than ANOTHER: 65 - master > v1.999.x > v1.55.x 66 - dev > v1.999.x > v1.55.x 67 - dev == master 68 69 2) Versions "dev-VERSION" behave the same as the VERSION: 70 - dev-master > v1.999.x > v1.55.x 71 - dev-master == dev == master 72 - v1.55.x > dev-v1.54.x > v1.53.x 73 - dev-v1.54.x == v1.54.x 74 75 3) Unspecified version (self.version is None) is treated as "master". 76 """ 77 if self.version in ('master', 'dev', 'dev-master', None): 78 return True 79 if another == 'master': 80 return False 81 return self._parse_version(self.version) >= self._parse_version(another) 82 83 def __str__(self): 84 return (f"TestConfig(client_lang='{self.client_lang}', " 85 f"server_lang='{self.server_lang}', version={self.version!r})") 86 87 @staticmethod 88 def _parse_version(version: str) -> pkg_version.Version: 89 if version.startswith('dev-'): 90 # Treat "dev-VERSION" as "VERSION". 91 version = version[4:] 92 if version.endswith('.x'): 93 version = version[:-2] 94 return pkg_version.Version(version) 95 96 97def _get_lang(image_name: str) -> Lang: 98 return Lang.from_string( 99 re.search(r'/(\w+)-(client|server):', image_name).group(1)) 100 101 102def evaluate_test_config(check: Callable[[TestConfig], bool]) -> TestConfig: 103 """Evaluates the test config check against Abseil flags. 104 105 TODO(sergiitk): split into parse_lang_spec and check_is_supported. 106 """ 107 # NOTE(lidiz) a manual skip mechanism is needed because absl/flags 108 # cannot be used in the built-in test-skipping decorators. See the 109 # official FAQs: 110 # https://abseil.io/docs/python/guides/flags#faqs 111 test_config = TestConfig( 112 client_lang=_get_lang(xds_k8s_flags.CLIENT_IMAGE.value), 113 server_lang=_get_lang(xds_k8s_flags.SERVER_IMAGE.value), 114 version=xds_flags.TESTING_VERSION.value) 115 if not check(test_config): 116 logger.info('Skipping %s', test_config) 117 raise unittest.SkipTest(f'Unsupported test config: {test_config}') 118 119 logger.info('Detected language and version: %s', test_config) 120 return test_config 121