1# TestFinder class, define set of tests to run. 2# 3# Copyright (c) 2020-2021 Virtuozzo International GmbH 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17# 18 19import os 20import glob 21import re 22from collections import defaultdict 23from contextlib import contextmanager 24from typing import Optional, List, Iterator, Set 25 26 27@contextmanager 28def chdir(path: Optional[str] = None) -> Iterator[None]: 29 if path is None: 30 yield 31 return 32 33 saved_dir = os.getcwd() 34 os.chdir(path) 35 try: 36 yield 37 finally: 38 os.chdir(saved_dir) 39 40 41class TestFinder: 42 def __init__(self, test_dir: Optional[str] = None) -> None: 43 self.groups = defaultdict(set) 44 45 with chdir(test_dir): 46 self.all_tests = glob.glob('[0-9][0-9][0-9]') 47 self.all_tests += [f for f in glob.iglob('tests/*') 48 if not f.endswith('.out') and 49 os.path.isfile(f + '.out')] 50 51 for t in self.all_tests: 52 with open(t, encoding="utf-8") as f: 53 for line in f: 54 if line.startswith('# group: '): 55 for g in line.split()[2:]: 56 self.groups[g].add(t) 57 break 58 59 def add_group_file(self, fname: str) -> None: 60 with open(fname, encoding="utf-8") as f: 61 for line in f: 62 line = line.strip() 63 64 if (not line) or line[0] == '#': 65 continue 66 67 words = line.split() 68 test_file = self.parse_test_name(words[0]) 69 groups = words[1:] 70 71 for g in groups: 72 self.groups[g].add(test_file) 73 74 def parse_test_name(self, name: str) -> str: 75 if '/' in name: 76 raise ValueError('Paths are unsupported for test selection, ' 77 f'requiring "{name}" is wrong') 78 79 if re.fullmatch(r'\d+', name): 80 # Numbered tests are old naming convention. We should convert them 81 # to three-digit-length, like 1 --> 001. 82 name = f'{int(name):03}' 83 else: 84 # Named tests all should be in tests/ subdirectory 85 name = os.path.join('tests', name) 86 87 if name not in self.all_tests: 88 raise ValueError(f'Test "{name}" is not found') 89 90 return name 91 92 def find_tests(self, groups: Optional[List[str]] = None, 93 exclude_groups: Optional[List[str]] = None, 94 tests: Optional[List[str]] = None, 95 start_from: Optional[str] = None) -> List[str]: 96 """Find tests 97 98 Algorithm: 99 100 1. a. if some @groups specified 101 a.1 Take all tests from @groups 102 a.2 Drop tests, which are in at least one of @exclude_groups or in 103 'disabled' group (if 'disabled' is not listed in @groups) 104 a.3 Add tests from @tests (don't exclude anything from them) 105 106 b. else, if some @tests specified: 107 b.1 exclude_groups must be not specified, so just take @tests 108 109 c. else (only @exclude_groups list is non-empty): 110 c.1 Take all tests 111 c.2 Drop tests, which are in at least one of @exclude_groups or in 112 'disabled' group 113 114 2. sort 115 116 3. If start_from specified, drop tests from first one to @start_from 117 (not inclusive) 118 """ 119 if groups is None: 120 groups = [] 121 if exclude_groups is None: 122 exclude_groups = [] 123 if tests is None: 124 tests = [] 125 126 res: Set[str] = set() 127 if groups: 128 # Some groups specified. exclude_groups supported, additionally 129 # selecting some individual tests supported as well. 130 res.update(*(self.groups[g] for g in groups)) 131 elif tests: 132 # Some individual tests specified, but no groups. In this case 133 # we don't support exclude_groups. 134 if exclude_groups: 135 raise ValueError("Can't exclude from individually specified " 136 "tests.") 137 else: 138 # No tests no groups: start from all tests, exclude_groups 139 # supported. 140 res.update(self.all_tests) 141 142 if 'disabled' not in groups and 'disabled' not in exclude_groups: 143 # Don't want to modify function argument, so create new list. 144 exclude_groups = exclude_groups + ['disabled'] 145 146 res = res.difference(*(self.groups[g] for g in exclude_groups)) 147 148 # We want to add @tests. But for compatibility with old test names, 149 # we should convert any number < 100 to number padded by 150 # leading zeroes, like 1 -> 001 and 23 -> 023. 151 for t in tests: 152 res.add(self.parse_test_name(t)) 153 154 sequence = sorted(res) 155 156 if start_from is not None: 157 del sequence[:sequence.index(self.parse_test_name(start_from))] 158 159 return sequence 160