160 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			160 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								# 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
							 |