The Gaudi Framework  master (57817a18)
Loading...
Searching...
No Matches
collect_for_ctest Namespace Reference

Functions

 pytest_addoption (parser, pluginmanager)
 pytest_collectstart (collector)
 pytest_collection_modifyitems (session, config, items)
 _resolve_sanitizer_lib (name)
 pytest_collection_finish (session)
 pytest_configure (config)

Variables

str TEST_DESC_TEMPLATE

Function Documentation

◆ _resolve_sanitizer_lib()

_resolve_sanitizer_lib ( name)
protected
Resolve a bare sanitizer runtime library name (e.g. "libasan.so") to
an absolute path by searching LD_LIBRARY_PATH ourselves, instead of
leaving it bare for the dynamic loader to search for later.

PRELOAD_SANITIZER_LIB (set by the sanitizer toolchain, see
lcg-toolchains' fragments/sanitizers/settings.cmake) is exported as a
bare filename. That's fine here: this function runs inside the same
`run`-wrapper environment used to launch this very pytest collection
process, which is known to have LD_LIBRARY_PATH set up correctly (the
sanitizer library does get found by *something* in this process, or
ASan wouldn't be able to report "runtime does not come first in
initial library list" at all -- it would fail to load entirely). But
that's not guaranteed for whatever process actually *runs* each test
later, which is a different invocation context (verified: pytest-based
CTest tests print "ld.so: ... cannot be preloaded (cannot open shared
object file): ignored" and silently run without the sanitizer active).
Resolving to an absolute path now, once, and baking it into each
test's ENVIRONMENT property removes that dependency entirely -- an
absolute path needs no search at test-run time.

Definition at line 109 of file collect_for_ctest.py.

109def _resolve_sanitizer_lib(name):
110 """Resolve a bare sanitizer runtime library name (e.g. "libasan.so") to
111 an absolute path by searching LD_LIBRARY_PATH ourselves, instead of
112 leaving it bare for the dynamic loader to search for later.
113
114 PRELOAD_SANITIZER_LIB (set by the sanitizer toolchain, see
115 lcg-toolchains' fragments/sanitizers/settings.cmake) is exported as a
116 bare filename. That's fine here: this function runs inside the same
117 `run`-wrapper environment used to launch this very pytest collection
118 process, which is known to have LD_LIBRARY_PATH set up correctly (the
119 sanitizer library does get found by *something* in this process, or
120 ASan wouldn't be able to report "runtime does not come first in
121 initial library list" at all -- it would fail to load entirely). But
122 that's not guaranteed for whatever process actually *runs* each test
123 later, which is a different invocation context (verified: pytest-based
124 CTest tests print "ld.so: ... cannot be preloaded (cannot open shared
125 object file): ignored" and silently run without the sanitizer active).
126 Resolving to an absolute path now, once, and baking it into each
127 test's ENVIRONMENT property removes that dependency entirely -- an
128 absolute path needs no search at test-run time.
129 """
130 if os.path.isabs(name):
131 return name
132 for directory in os.environ.get("LD_LIBRARY_PATH", "").split(":"):
133 if directory and os.path.isfile(os.path.join(directory, name)):
134 return os.path.join(directory, name)
135 return name # not found: fall back to the bare name, as before
136
137

◆ pytest_addoption()

pytest_addoption ( parser,
pluginmanager )

Definition at line 24 of file collect_for_ctest.py.

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

◆ pytest_collection_finish()

pytest_collection_finish ( session)

Definition at line 138 of file collect_for_ctest.py.

138def pytest_collection_finish(session):
139 args = session.ctest_args
140 output_filename = args.get("output_file")
141
142 if not output_filename:
143 # nothing to do if no output file is specified
144 return
145
146 output = open(output_filename, "w")
147 output_rootdir = Path(output_filename).parent
148
149 coverage = args["coverage"].split(",") if args["coverage"] else []
150 if coverage:
151 args["pytest_command"] += " --cov-report= --cov-reset " + " ".join(
152 f"--cov={module}" for module in coverage
153 )
154
155 properties = 'LABELS "{}" '.format(";".join(args["label"]))
156 env_entries = []
157 if args.get("binary_dir"):
158 env_entries.append(f"CMAKE_CURRENT_BINARY_DIR={args['binary_dir']}")
159 preload_sanitizer_lib = os.environ.get("PRELOAD_SANITIZER_LIB")
160 if preload_sanitizer_lib:
161 resolved = ":".join(
162 _resolve_sanitizer_lib(name)
163 for name in preload_sanitizer_lib.split(":")
164 if name
165 )
166 env_entries.append(f"LD_PRELOAD={resolved}")
167 if env_entries:
168 properties += 'ENVIRONMENT "{}" '.format(";".join(env_entries))
169 properties += " ".join(args["properties"])
170
171 producers = defaultdict(list) # test name -> list of fixtures it produces
172 consumers = defaultdict(list) # test name -> list of fixtures it depends on
173 fixtures = defaultdict(list) # fixture name -> list of tests that produce it
174 names = []
175 for path in sorted(session.ctest_files):
176 name = os.path.relpath(path, args["pytest_root_dir"]).replace("/", ".")
177 if name.endswith(".py"):
178 name = name[:-3]
179 if name == ".":
180 # when we pass a single file to pytest, pytest_root_dir and path are the same
181 # and relpath returns ".", so we take the prefix and drop the final dot
182 # (see https://gitlab.cern.ch/gaudi/Gaudi/-/issues/354)
183 name = args["prefix"][:-1]
184 else:
185 name = args["prefix"] + name
186 pytest_cmd = args["pytest_command"]
187
188 output.write(
189 TEST_DESC_TEMPLATE.format(
190 name=name,
191 path=path,
192 pytest_cmd=pytest_cmd,
193 properties=properties,
194 )
195 )
196
197 if session.ctest_fixture_setup.get(path):
198 for fixture in session.ctest_fixture_setup[path]:
199 producers[name].append(fixture)
200 fixtures[fixture].append(name)
201
202 if session.ctest_fixture_required.get(path):
203 for fixture in session.ctest_fixture_required[path]:
204 consumers[name].append(fixture)
205
206 names.append(name)
207
208 for name in names:
209 if name in producers:
210 output.write(
211 'set_tests_properties('
212 f'{name} PROPERTIES FIXTURES_SETUP "{";".join(producers[name])}")\n'
213 )
214
215 if name in consumers:
216 producer_test_names = []
217 for fixture in consumers[name]:
218 producer_test_names.extend(fixtures[fixture])
219 output.write(
220 'if (DEFINED ENV{PYTEST_DISABLE_FIXTURES_REQUIRED})\n'
221 f' set_tests_properties({name} PROPERTIES DEPENDS "{";".join(producer_test_names)}")\n'
222 'else()\n'
223 f' set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED "{";".join(consumers[name])}")\n'
224 'endif()\n'
225 )
226
227 # we force one test to be run one by one
228 if coverage:
229 # generate one coverage file per test
230 output.write(
231 f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage.{name})\n"
232 f"set_tests_properties({name} PROPERTIES FIXTURES_SETUP {name})\n"
233 )
234
235 if coverage and names:
236 combine_test = (args["prefix"] + "coverage_combine").replace("/", ".")
237 combine_command = re.sub(
238 r" report .*",
239 f" combine {' '.join(f'{output_rootdir}/.coverage.{n}' for n in names)}",
240 args["coverage_command"],
241 )
242 output.write(
243 TEST_DESC_TEMPLATE.format(
244 name=combine_test,
245 path="",
246 pytest_cmd=combine_command,
247 properties=properties,
248 )
249 )
250 output.write(
251 f"set_tests_properties({combine_test} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
252 f"set_tests_properties({combine_test} PROPERTIES FIXTURES_REQUIRED \"{';'.join(names)}\")\n"
253 f"set_tests_properties({combine_test} PROPERTIES FIXTURES_SETUP {combine_test})\n"
254 )
255
256 name = (args["prefix"] + "coverage_report").replace("/", ".")
257 output.write(
258 TEST_DESC_TEMPLATE.format(
259 name=name,
260 path="",
261 pytest_cmd=f"{args['coverage_command']}",
262 properties=properties + " LABELS coverage",
263 )
264 )
265 # coverage reports require all related tests to be run first
266 output.write(
267 f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
268 f"set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED {combine_test})\n"
269 )
270
271 output.close()
272
273
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition MsgStream.cpp:93

◆ pytest_collection_modifyitems()

pytest_collection_modifyitems ( session,
config,
items )

Definition at line 89 of file collect_for_ctest.py.

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

◆ pytest_collectstart()

pytest_collectstart ( collector)

Definition at line 73 of file collect_for_ctest.py.

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

◆ pytest_configure()

pytest_configure ( config)

Definition at line 274 of file collect_for_ctest.py.

274def pytest_configure(config):
275 config.addinivalue_line(
276 "markers",
277 "ctest_fixture_setup(name): mark test to set up a fixture needed by another test",
278 )
279 config.addinivalue_line(
280 "markers",
281 "ctest_fixture_required(name): mark test to require a fixture set up by another test",
282 )

Variable Documentation

◆ TEST_DESC_TEMPLATE

str collect_for_ctest.TEST_DESC_TEMPLATE
Initial value:
= """
add_test({name} {pytest_cmd} {path})
set_tests_properties({name} PROPERTIES {properties})
"""

Definition at line 103 of file collect_for_ctest.py.