| # TestFinder class, define set of tests to run. |
| # |
| # Copyright (c) 2020-2021 Virtuozzo International GmbH |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| # |
| |
| import os |
| import glob |
| import re |
| from collections import defaultdict |
| from contextlib import contextmanager |
| from typing import Optional, List, Iterator, Set |
| |
| |
| @contextmanager |
| def chdir(path: Optional[str] = None) -> Iterator[None]: |
| if path is None: |
| yield |
| return |
| |
| saved_dir = os.getcwd() |
| os.chdir(path) |
| try: |
| yield |
| finally: |
| os.chdir(saved_dir) |
| |
| |
| class TestFinder: |
| def __init__(self, test_dir: Optional[str] = None) -> None: |
| self.groups = defaultdict(set) |
| |
| with chdir(test_dir): |
| self.all_tests = glob.glob('[0-9][0-9][0-9]') |
| self.all_tests += [f for f in glob.iglob('tests/*') |
| if not f.endswith('.out') and |
| os.path.isfile(f + '.out')] |
| |
| for t in self.all_tests: |
| with open(t, encoding="utf-8") as f: |
| for line in f: |
| if line.startswith('# group: '): |
| for g in line.split()[2:]: |
| self.groups[g].add(t) |
| break |
| |
| def add_group_file(self, fname: str) -> None: |
| with open(fname, encoding="utf-8") as f: |
| for line in f: |
| line = line.strip() |
| |
| if (not line) or line[0] == '#': |
| continue |
| |
| words = line.split() |
| test_file = self.parse_test_name(words[0]) |
| groups = words[1:] |
| |
| for g in groups: |
| self.groups[g].add(test_file) |
| |
| def parse_test_name(self, name: str) -> str: |
| if '/' in name: |
| raise ValueError('Paths are unsupported for test selection, ' |
| f'requiring "{name}" is wrong') |
| |
| if re.fullmatch(r'\d+', name): |
| # Numbered tests are old naming convention. We should convert them |
| # to three-digit-length, like 1 --> 001. |
| name = f'{int(name):03}' |
| else: |
| # Named tests all should be in tests/ subdirectory |
| name = os.path.join('tests', name) |
| |
| if name not in self.all_tests: |
| raise ValueError(f'Test "{name}" is not found') |
| |
| return name |
| |
| def find_tests(self, groups: Optional[List[str]] = None, |
| exclude_groups: Optional[List[str]] = None, |
| tests: Optional[List[str]] = None, |
| start_from: Optional[str] = None) -> List[str]: |
| """Find tests |
| |
| Algorithm: |
| |
| 1. a. if some @groups specified |
| a.1 Take all tests from @groups |
| a.2 Drop tests, which are in at least one of @exclude_groups or in |
| 'disabled' group (if 'disabled' is not listed in @groups) |
| a.3 Add tests from @tests (don't exclude anything from them) |
| |
| b. else, if some @tests specified: |
| b.1 exclude_groups must be not specified, so just take @tests |
| |
| c. else (only @exclude_groups list is non-empty): |
| c.1 Take all tests |
| c.2 Drop tests, which are in at least one of @exclude_groups or in |
| 'disabled' group |
| |
| 2. sort |
| |
| 3. If start_from specified, drop tests from first one to @start_from |
| (not inclusive) |
| """ |
| if groups is None: |
| groups = [] |
| if exclude_groups is None: |
| exclude_groups = [] |
| if tests is None: |
| tests = [] |
| |
| res: Set[str] = set() |
| if groups: |
| # Some groups specified. exclude_groups supported, additionally |
| # selecting some individual tests supported as well. |
| res.update(*(self.groups[g] for g in groups)) |
| elif tests: |
| # Some individual tests specified, but no groups. In this case |
| # we don't support exclude_groups. |
| if exclude_groups: |
| raise ValueError("Can't exclude from individually specified " |
| "tests.") |
| else: |
| # No tests no groups: start from all tests, exclude_groups |
| # supported. |
| res.update(self.all_tests) |
| |
| if 'disabled' not in groups and 'disabled' not in exclude_groups: |
| # Don't want to modify function argument, so create new list. |
| exclude_groups = exclude_groups + ['disabled'] |
| |
| res = res.difference(*(self.groups[g] for g in exclude_groups)) |
| |
| # We want to add @tests. But for compatibility with old test names, |
| # we should convert any number < 100 to number padded by |
| # leading zeroes, like 1 -> 001 and 23 -> 023. |
| for t in tests: |
| res.add(self.parse_test_name(t)) |
| |
| sequence = sorted(res) |
| |
| if start_from is not None: |
| del sequence[:sequence.index(self.parse_test_name(start_from))] |
| |
| return sequence |