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