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