The Gaudi Framework  master (ff829712)
Loading...
Searching...
No Matches
collect_for_ctest.py
Go to the documentation of this file.
11"""
12pytest plugin that report collected pytest files as CTest tests
13
14This plugin is not meant to be used directly, but it is invoked by the
15CMake function `gaudi_add_pytest()`
16"""
17
18import os
19import re
20from collections import defaultdict
21from pathlib import Path
22
23
24def pytest_addoption(parser, pluginmanager):
25 parser.addoption(
26 "--ctest-output-file",
27 help="name of the file to write to communicate to ctest the discovered tests",
28 )
29 parser.addoption(
30 "--ctest-pytest-command",
31 default="pytest",
32 help="how pytest has to be invoked (e.g. using wrapper commands)",
33 )
34 parser.addoption(
35 "--ctest-pytest-root-dir",
36 default=Path.cwd(),
37 help="root directory to compute test names",
38 )
39 parser.addoption(
40 "--ctest-prefix",
41 default="",
42 help="string to prefix to the generated test names",
43 )
44 parser.addoption(
45 "--ctest-label",
46 default=["pytest"],
47 action="append",
48 help="labels to attach to the test (the label pytest is always added)",
49 )
50 parser.addoption(
51 "--ctest-properties",
52 default=[],
53 action="append",
54 help="test properties to set for all discovered tests",
55 )
56 parser.addoption(
57 "--ctest-binary-dir",
58 default=None,
59 help="value of CMAKE_CURRENT_BINARY_DIR from which gaudi_add_pytest was invoked",
60 )
61 parser.addoption(
62 "--ctest-coverage",
63 default="",
64 help="select modules for which produce coverage reports",
65 )
66 parser.addoption(
67 "--ctest-coverage-command",
68 default="coverage report",
69 help="how coverage should be invoked to produce the final report",
70 )
71
72
73def pytest_collectstart(collector):
74 session, config = collector.session, collector.config
75 args = {
76 name[6:]: getattr(config.option, name)
77 for name in dir(config.option)
78 if name.startswith("ctest_")
79 }
80
81 if args.get("binary_dir"):
82 # $CMAKE_CURRENT_BINARY_DIR will be set by CTest when the test is run
83 # so, for consistency, we set it at collection time too
84 os.environ["CMAKE_CURRENT_BINARY_DIR"] = args["binary_dir"]
85
86 session.ctest_args = args
87
88
89def pytest_collection_modifyitems(session, config, items):
90 if not session.ctest_args.get("output_file"):
91 # nothing to do if no output file is specified
92 return
93
94 session.ctest_files = set(item.path for item in items)
95 session.ctest_fixture_setup = defaultdict(set)
96 session.ctest_fixture_required = defaultdict(set)
97 for item in items:
98 for marker in ("ctest_fixture_setup", "ctest_fixture_required"):
99 for mark in item.iter_markers(name=marker):
100 getattr(session, marker)[item.path].update(mark.args)
101
102
103TEST_DESC_TEMPLATE = """
104add_test({name} {pytest_cmd} {path})
105set_tests_properties({name} PROPERTIES {properties})
106"""
107
108
110 args = session.ctest_args
111 output_filename = args.get("output_file")
112
113 if not output_filename:
114 # nothing to do if no output file is specified
115 return
116
117 output = open(output_filename, "w")
118 output_rootdir = Path(output_filename).parent
119
120 coverage = args["coverage"].split(",") if args["coverage"] else []
121 if coverage:
122 args["pytest_command"] += " --cov-report= --cov-reset " + " ".join(
123 f"--cov={module}" for module in coverage
124 )
125
126 properties = 'LABELS "{}" '.format(";".join(args["label"]))
127 if args.get("binary_dir"):
128 properties += f'ENVIRONMENT "CMAKE_CURRENT_BINARY_DIR={args["binary_dir"]}" '
129 properties += " ".join(args["properties"])
130
131 producers = defaultdict(list) # test name -> list of fixtures it produces
132 consumers = defaultdict(list) # test name -> list of fixtures it depends on
133 fixtures = defaultdict(list) # fixture name -> list of tests that produce it
134 names = []
135 for path in sorted(session.ctest_files):
136 name = os.path.relpath(path, args["pytest_root_dir"]).replace("/", ".")
137 if name.endswith(".py"):
138 name = name[:-3]
139 if name == ".":
140 # when we pass a single file to pytest, pytest_root_dir and path are the same
141 # and relpath returns ".", so we take the prefix and drop the final dot
142 # (see https://gitlab.cern.ch/gaudi/Gaudi/-/issues/354)
143 name = args["prefix"][:-1]
144 else:
145 name = args["prefix"] + name
146 pytest_cmd = args["pytest_command"]
147
148 output.write(
149 TEST_DESC_TEMPLATE.format(
150 name=name,
151 path=path,
152 pytest_cmd=pytest_cmd,
153 properties=properties,
154 )
155 )
156
157 if session.ctest_fixture_setup.get(path):
158 for fixture in session.ctest_fixture_setup[path]:
159 producers[name].append(fixture)
160 fixtures[fixture].append(name)
161
162 if session.ctest_fixture_required.get(path):
163 for fixture in session.ctest_fixture_required[path]:
164 consumers[name].append(fixture)
165
166 names.append(name)
167
168 for name in names:
169 if name in producers:
170 output.write(
171 'set_tests_properties('
172 f'{name} PROPERTIES FIXTURES_SETUP "{";".join(producers[name])}")\n'
173 )
174
175 if name in consumers:
176 producer_test_names = []
177 for fixture in consumers[name]:
178 producer_test_names.extend(fixtures[fixture])
179 output.write(
180 'if (DEFINED ENV{PYTEST_DISABLE_FIXTURES_REQUIRED})\n'
181 f' set_tests_properties({name} PROPERTIES DEPENDS "{";".join(producer_test_names)}")\n'
182 'else()\n'
183 f' set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED "{";".join(consumers[name])}")\n'
184 'endif()\n'
185 )
186
187 # we force one test to be run one by one
188 if coverage:
189 # generate one coverage file per test
190 output.write(
191 f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage.{name})\n"
192 f"set_tests_properties({name} PROPERTIES FIXTURES_SETUP {name})\n"
193 )
194
195 if coverage and names:
196 combine_test = (args["prefix"] + "coverage_combine").replace("/", ".")
197 combine_command = re.sub(
198 r" report .*",
199 f" combine {' '.join(f'{output_rootdir}/.coverage.{n}' for n in names)}",
200 args["coverage_command"],
201 )
202 output.write(
203 TEST_DESC_TEMPLATE.format(
204 name=combine_test,
205 path="",
206 pytest_cmd=combine_command,
207 properties=properties,
208 )
209 )
210 output.write(
211 f"set_tests_properties({combine_test} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
212 f"set_tests_properties({combine_test} PROPERTIES FIXTURES_REQUIRED \"{';'.join(names)}\")\n"
213 f"set_tests_properties({combine_test} PROPERTIES FIXTURES_SETUP {combine_test})\n"
214 )
215
216 name = (args["prefix"] + "coverage_report").replace("/", ".")
217 output.write(
218 TEST_DESC_TEMPLATE.format(
219 name=name,
220 path="",
221 pytest_cmd=f"{args['coverage_command']}",
222 properties=properties + " LABELS coverage",
223 )
224 )
225 # coverage reports require all related tests to be run first
226 output.write(
227 f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
228 f"set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED {combine_test})\n"
229 )
230
231 output.close()
232
233
235 config.addinivalue_line(
236 "markers",
237 "ctest_fixture_setup(name): mark test to set up a fixture needed by another test",
238 )
239 config.addinivalue_line(
240 "markers",
241 "ctest_fixture_required(name): mark test to require a fixture set up by another test",
242 )
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition MsgStream.cpp:93
pytest_collection_finish(session)
pytest_collection_modifyitems(session, config, items)
pytest_addoption(parser, pluginmanager)
pytest_collectstart(collector)