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