diff --git a/tools/run_tests/xds_k8s_test_driver/framework/helpers/skips.py b/tools/run_tests/xds_k8s_test_driver/framework/helpers/skips.py index efea6b07726..3cf52ac9379 100644 --- a/tools/run_tests/xds_k8s_test_driver/framework/helpers/skips.py +++ b/tools/run_tests/xds_k8s_test_driver/framework/helpers/skips.py @@ -13,8 +13,10 @@ # limitations under the License. """The classes and predicates to assist validate test config for test cases.""" from dataclasses import dataclass +import enum +import logging import re -from typing import Callable +from typing import Callable, Optional import unittest from packaging import version as pkg_version @@ -22,35 +24,75 @@ from packaging import version as pkg_version from framework import xds_flags from framework import xds_k8s_flags +logger = logging.getLogger(__name__) -def _get_lang(image_name: str) -> str: - return re.search(r'/(\w+)-(client|server):', image_name).group(1) +class Lang(enum.Flag): + UNKNOWN = enum.auto() + CPP = enum.auto() + GO = enum.auto() + JAVA = enum.auto() + PYTHON = enum.auto() + NODE = enum.auto() -def _parse_version(s: str) -> pkg_version.Version: - if s.endswith(".x"): - s = s[:-2] - return pkg_version.Version(s) + def __str__(self): + return str(self.name).lower() + + @classmethod + def from_string(cls, lang: str): + try: + return cls[lang.upper()] + except KeyError: + return cls.UNKNOWN @dataclass class TestConfig: """Describes the config for the test suite.""" - client_lang: str - server_lang: str - version: str + client_lang: Lang + server_lang: Lang + version: Optional[str] - def version_ge(self, another: str) -> bool: + def version_gte(self, another: str) -> bool: """Returns a bool for whether the version is >= another one. A version is greater than or equal to another version means its version number is greater than or equal to another version's number. Version - "master" is always considered latest. E.g., master >= v1.41.x >= v1.40.x - >= v1.9.x. + "master" is always considered latest. + E.g., master >= v1.41.x >= v1.40.x >= v1.9.x. + + Unspecified version is treated as 'master', but isn't explicitly set. """ - if self.version == 'master': + if self.version == 'master' or self.version is None: return True - return _parse_version(self.version) >= _parse_version(another) + return self._parse_version(self.version) >= self._parse_version(another) + + def version_lt(self, another: str) -> bool: + """Returns a bool for whether the version is < another one. + + Version "master" is always considered latest. + E.g., v1.9.x < v1.40.x < v1.41.x < master. + + Unspecified version is treated as 'master', but isn't explicitly set. + """ + if self.version == 'master' or self.version is None: + return False + return self._parse_version(self.version) < self._parse_version(another) + + def __str__(self): + return (f"TestConfig(client_lang='{self.client_lang}', " + f"server_lang='{self.server_lang}', version={self.version!r})") + + @staticmethod + def _parse_version(s: str) -> pkg_version.Version: + if s.endswith(".x"): + s = s[:-2] + return pkg_version.Version(s) + + +def _get_lang(image_name: str) -> Lang: + return Lang.from_string( + re.search(r'/(\w+)-(client|server):', image_name).group(1)) def evaluate_test_config(check: Callable[[TestConfig], bool]) -> None: @@ -64,4 +106,7 @@ def evaluate_test_config(check: Callable[[TestConfig], bool]) -> None: server_lang=_get_lang(xds_k8s_flags.SERVER_IMAGE.value), version=xds_flags.TESTING_VERSION.value) if not check(test_config): + logger.info('Skipping %s', test_config) raise unittest.SkipTest(f'Unsupported test config: {test_config}') + + logger.info('Detected language and version: %s', test_config) diff --git a/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py b/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py index e6412da4c67..2b411b1643c 100644 --- a/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py +++ b/tools/run_tests/xds_k8s_test_driver/framework/xds_flags.py @@ -116,7 +116,7 @@ CLIENT_PORT = flags.DEFINE_integer( # Testing metadata TESTING_VERSION = flags.DEFINE_string( "testing_version", - default="master", + default=None, help="The testing gRPC version branch name. Like master, v1.41.x, v1.37.x") FORCE_CLEANUP = flags.DEFINE_bool( diff --git a/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py b/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py index 7b7561580ca..fcf394a5da7 100644 --- a/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py +++ b/tools/run_tests/xds_k8s_test_driver/framework/xds_k8s_testcase.py @@ -85,7 +85,7 @@ class XdsKubernetesBaseTestCase(absltest.TestCase): td: TrafficDirectorManager @staticmethod - def isSupported(config: skips.TestConfig) -> bool: + def is_supported(config: skips.TestConfig) -> bool: """Overridden by the test class to decide if the config is supported. Returns: @@ -104,7 +104,7 @@ class XdsKubernetesBaseTestCase(absltest.TestCase): # Raises unittest.SkipTest if given client/server/version does not # support current test case. - skips.evaluate_test_config(cls.isSupported) + skips.evaluate_test_config(cls.is_supported) # GCP cls.project: str = xds_flags.PROJECT.value diff --git a/tools/run_tests/xds_k8s_test_driver/tests/affinity_test.py b/tools/run_tests/xds_k8s_test_driver/tests/affinity_test.py index eef8f15e26b..27b18a825a1 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/affinity_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/affinity_test.py @@ -31,6 +31,7 @@ flags.adopt_module_key_flags(xds_k8s_testcase) _XdsTestServer = xds_k8s_testcase.XdsTestServer _XdsTestClient = xds_k8s_testcase.XdsTestClient _ChannelzChannelState = grpc_channelz.ChannelState +_Lang = skips.Lang # Testing consts _TEST_AFFINITY_METADATA_KEY = 'xds_md' @@ -43,10 +44,12 @@ _RPC_COUNT = 100 class AffinityTest(xds_k8s_testcase.RegularXdsKubernetesTestCase): @staticmethod - def isSupported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'java', 'python', 'go']: - return config.version_ge('v1.40.x') - return False + def is_supported(config: skips.TestConfig) -> bool: + if config.client_lang in (_Lang.CPP | _Lang.GO | _Lang.JAVA | + _Lang.PYTHON): + # Versions prior to v1.40.x don't support Affinity. + return not config.version_lt('v1.40.x') + return True def test_affinity(self) -> None: # pylint: disable=too-many-statements diff --git a/tools/run_tests/xds_k8s_test_driver/tests/authz_test.py b/tools/run_tests/xds_k8s_test_driver/tests/authz_test.py index 47efa4399ab..ffb474fa12c 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/authz_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/authz_test.py @@ -29,6 +29,7 @@ flags.adopt_module_key_flags(xds_k8s_testcase) _XdsTestServer = xds_k8s_testcase.XdsTestServer _XdsTestClient = xds_k8s_testcase.XdsTestClient _SecurityMode = xds_k8s_testcase.SecurityXdsKubernetesTestCase.SecurityMode +_Lang = skips.Lang # The client generates QPS even when it is still loading information from xDS. # Once it finally connects there will be an outpouring of the bufferred RPCs and @@ -47,12 +48,14 @@ class AuthzTest(xds_k8s_testcase.SecurityXdsKubernetesTestCase): } @staticmethod - def isSupported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'python']: - return config.version_ge('v1.44.x') - elif config.client_lang in ['java', 'go']: - return config.version_ge('v1.42.x') - return False + def is_supported(config: skips.TestConfig) -> bool: + # Per "Authorization (RBAC)" in + # https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md + if config.client_lang in _Lang.CPP | _Lang.PYTHON: + return not config.version_lt('v1.44.x') + elif config.client_lang in _Lang.GO | _Lang.JAVA: + return not config.version_lt('v1.42.x') + return True def setUp(self): super().setUp() diff --git a/tools/run_tests/xds_k8s_test_driver/tests/security_test.py b/tools/run_tests/xds_k8s_test_driver/tests/security_test.py index 0d70f3083df..8ef0edc4ea7 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/security_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/security_test.py @@ -27,15 +27,19 @@ flags.adopt_module_key_flags(xds_k8s_testcase) _XdsTestServer = xds_k8s_testcase.XdsTestServer _XdsTestClient = xds_k8s_testcase.XdsTestClient _SecurityMode = xds_k8s_testcase.SecurityXdsKubernetesTestCase.SecurityMode +_Lang = skips.Lang class SecurityTest(xds_k8s_testcase.SecurityXdsKubernetesTestCase): @staticmethod - def isSupported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'python', 'go']: - return config.version_ge('v1.41.x') - return False + def is_supported(config: skips.TestConfig) -> bool: + if config.client_lang in (_Lang.CPP | _Lang.GO | _Lang.JAVA | + _Lang.PYTHON): + # Versions prior to v1.41.x don't support PSM Security. + # https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md + return not config.version_lt('v1.41.x') + return True def test_mtls(self): """mTLS test. diff --git a/tools/run_tests/xds_k8s_test_driver/tests/subsetting_test.py b/tools/run_tests/xds_k8s_test_driver/tests/subsetting_test.py index b6292037047..abe537323df 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/subsetting_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/subsetting_test.py @@ -38,11 +38,11 @@ _NUM_CLIENTS = 3 class SubsettingTest(xds_k8s_testcase.RegularXdsKubernetesTestCase): @staticmethod - def isSupported(config: skips.TestConfig) -> bool: + def is_supported(config: skips.TestConfig) -> bool: # Subsetting is an experimental feature where most work is done on the # server-side. We limit it to only run on master branch to save # resources. - return config.version_ge('master') + return config.version_gte('master') def test_subsetting_basic(self) -> None: with self.subTest('00_create_health_check'): diff --git a/tools/run_tests/xds_k8s_test_driver/tests/url_map/affinity_test.py b/tools/run_tests/xds_k8s_test_driver/tests/url_map/affinity_test.py index fe079bbc5e3..6a29f227856 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/url_map/affinity_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/url_map/affinity_test.py @@ -31,6 +31,7 @@ DumpedXdsConfig = xds_url_map_testcase.DumpedXdsConfig RpcTypeUnaryCall = xds_url_map_testcase.RpcTypeUnaryCall RpcTypeEmptyCall = xds_url_map_testcase.RpcTypeEmptyCall XdsTestClient = client_app.XdsTestClient +_Lang = skips.Lang logger = logging.getLogger(__name__) flags.adopt_module_key_flags(xds_url_map_testcase) @@ -52,15 +53,25 @@ _TEST_METADATA = ( _ChannelzChannelState = grpc_channelz.ChannelState +def _is_supported(config: skips.TestConfig) -> bool: + # Per "Ring hash" in + # https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md + if config.client_lang in _Lang.CPP | _Lang.JAVA: + return not config.version_lt('v1.40.x') + elif config.client_lang == _Lang.GO: + return not config.version_lt('v1.41.x') + elif config.client_lang == _Lang.PYTHON: + # TODO(https://github.com/grpc/grpc/issues/27430): supported after + # the issue is fixed. + return False + return True + + class TestHeaderBasedAffinity(xds_url_map_testcase.XdsUrlMapTestCase): @staticmethod def is_supported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'java']: - return config.version_ge('v1.40.x') - if config.client_lang in ['go']: - return config.version_ge('v1.41.x') - return False + return _is_supported(config) @staticmethod def client_init_config(rpc: str, metadata: str): @@ -125,11 +136,7 @@ class TestHeaderBasedAffinityMultipleHeaders( @staticmethod def is_supported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'java']: - return config.version_ge('v1.40.x') - if config.client_lang in ['go']: - return config.version_ge('v1.41.x') - return False + return _is_supported(config) @staticmethod def client_init_config(rpc: str, metadata: str): diff --git a/tools/run_tests/xds_k8s_test_driver/tests/url_map/retry_test.py b/tools/run_tests/xds_k8s_test_driver/tests/url_map/retry_test.py index 8929e99c1b2..89fb34ca07d 100644 --- a/tools/run_tests/xds_k8s_test_driver/tests/url_map/retry_test.py +++ b/tools/run_tests/xds_k8s_test_driver/tests/url_map/retry_test.py @@ -30,6 +30,7 @@ DumpedXdsConfig = xds_url_map_testcase.DumpedXdsConfig RpcTypeUnaryCall = xds_url_map_testcase.RpcTypeUnaryCall XdsTestClient = client_app.XdsTestClient ExpectedResult = xds_url_map_testcase.ExpectedResult +_Lang = skips.Lang logger = logging.getLogger(__name__) flags.adopt_module_key_flags(xds_url_map_testcase) @@ -62,15 +63,21 @@ def _build_retry_route_rule(retryConditions, num_retries): } +def _is_supported(config: skips.TestConfig) -> bool: + # Per "Retry" in + # https://github.com/grpc/grpc/blob/master/doc/grpc_xds_features.md + if config.client_lang in _Lang.CPP | _Lang.JAVA | _Lang.PYTHON: + return not config.version_lt('v1.40.x') + elif config.client_lang == _Lang.GO: + return not config.version_lt('v1.41.x') + return True + + class TestRetryUpTo3AttemptsAndFail(xds_url_map_testcase.XdsUrlMapTestCase): @staticmethod def is_supported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'java', 'python']: - return config.version_ge('v1.40.x') - elif config.client_lang == 'go': - return config.version_ge('v1.41.x') - return False + return _is_supported(config) @staticmethod def url_map_change( @@ -111,11 +118,7 @@ class TestRetryUpTo4AttemptsAndSucceed(xds_url_map_testcase.XdsUrlMapTestCase): @staticmethod def is_supported(config: skips.TestConfig) -> bool: - if config.client_lang in ['cpp', 'java', 'python']: - return config.version_ge('v1.40.x') - elif config.client_lang == 'go': - return config.version_ge('v1.41.x') - return False + return _is_supported(config) @staticmethod def url_map_change(