Merge tag 'pull-request-2024-10-21' of https://gitlab.com/thuth/qemu into staging
[qemu/kevin.git] / tests / qemu-iotests / findtests.py
blobdd77b453b8ad409906a133c7cd18e583067f4f17
1 # TestFinder class, define set of tests to run.
3 # Copyright (c) 2020-2021 Virtuozzo International GmbH
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.
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.
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/>.
19 import os
20 import glob
21 import re
22 from collections import defaultdict
23 from contextlib import contextmanager
24 from typing import Optional, List, Iterator, Set
27 @contextmanager
28 def chdir(path: Optional[str] = None) -> Iterator[None]:
29 if path is None:
30 yield
31 return
33 saved_dir = os.getcwd()
34 os.chdir(path)
35 try:
36 yield
37 finally:
38 os.chdir(saved_dir)
41 class TestFinder:
42 def __init__(self, test_dir: Optional[str] = None) -> None:
43 self.groups = defaultdict(set)
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')]
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
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()
64 if (not line) or line[0] == '#':
65 continue
67 words = line.split()
68 test_file = self.parse_test_name(words[0])
69 groups = words[1:]
71 for g in groups:
72 self.groups[g].add(test_file)
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')
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)
87 if name not in self.all_tests:
88 raise ValueError(f'Test "{name}" is not found')
90 return name
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
98 Algorithm:
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)
106 b. else, if some @tests specified:
107 b.1 exclude_groups must be not specified, so just take @tests
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
114 2. sort
116 3. If start_from specified, drop tests from first one to @start_from
117 (not inclusive)
119 if groups is None:
120 groups = []
121 if exclude_groups is None:
122 exclude_groups = []
123 if tests is None:
124 tests = []
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)
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']
146 res = res.difference(*(self.groups[g] for g in exclude_groups))
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))
154 sequence = sorted(res)
156 if start_from is not None:
157 del sequence[:sequence.index(self.parse_test_name(start_from))]
159 return sequence