The Gaudi Framework  v39r3 (979e3109)
collect_for_ctest.py
Go to the documentation of this file.
1 
11 """
12 pytest plugin that report collected pytest files as CTest tests
13 
14 This plugin is not meant to be used directly, but it is invoked by the
15 CMake function `gaudi_add_pytest()`
16 """
17 
18 import os
19 import re
20 from collections import defaultdict
21 from pathlib import Path
22 
23 
24 def 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 
73 def 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 
89 def 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 
103 TEST_DESC_TEMPLATE = """
104 add_test({name} {pytest_cmd} {path})
105 set_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 = (
137  args["prefix"] + os.path.relpath(path, args["pytest_root_dir"])
138  ).replace("/", ".")
139  if name.endswith(".py"):
140  name = name[:-3]
141  pytest_cmd = args["pytest_command"]
142 
143  output.write(
144  TEST_DESC_TEMPLATE.format(
145  name=name,
146  path=path,
147  pytest_cmd=pytest_cmd,
148  properties=properties,
149  )
150  )
151 
152  if session.ctest_fixture_setup.get(path):
153  for fixture in session.ctest_fixture_setup[path]:
154  producers[name].append(fixture)
155  fixtures[fixture].append(name)
156 
157  if session.ctest_fixture_required.get(path):
158  for fixture in session.ctest_fixture_required[path]:
159  consumers[name].append(fixture)
160 
161  names.append(name)
162 
163  for name in names:
164  if name in producers:
165  output.write(
166  'set_tests_properties('
167  f'{name} PROPERTIES FIXTURES_SETUP "{";".join(producers[name])}")\n'
168  )
169 
170  if name in consumers:
171  producer_test_names = []
172  for fixture in consumers[name]:
173  producer_test_names.extend(fixtures[fixture])
174  output.write(
175  'if (DEFINED ENV{PYTEST_DISABLE_FIXTURES_REQUIRED})\n'
176  f' set_tests_properties({name} PROPERTIES DEPENDS "{";".join(producer_test_names)}")\n'
177  'else()\n'
178  f' set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED "{";".join(consumers[name])}")\n'
179  'endif()\n'
180  )
181 
182  # we force one test to be run one by one
183  if coverage:
184  # generate one coverage file per test
185  output.write(
186  f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage.{name})\n"
187  f"set_tests_properties({name} PROPERTIES FIXTURES_SETUP {name})\n"
188  )
189 
190  if coverage and names:
191  combine_test = (args["prefix"] + "coverage_combine").replace("/", ".")
192  combine_command = re.sub(
193  r" report .*",
194  f" combine {' '.join(f'{output_rootdir}/.coverage.{n}' for n in names)}",
195  args["coverage_command"],
196  )
197  output.write(
198  TEST_DESC_TEMPLATE.format(
199  name=combine_test,
200  path="",
201  pytest_cmd=combine_command,
202  properties=properties,
203  )
204  )
205  output.write(
206  f"set_tests_properties({combine_test} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
207  f"set_tests_properties({combine_test} PROPERTIES FIXTURES_REQUIRED \"{';'.join(names)}\")\n"
208  f"set_tests_properties({combine_test} PROPERTIES FIXTURES_SETUP {combine_test})\n"
209  )
210 
211  name = (args["prefix"] + "coverage_report").replace("/", ".")
212  output.write(
213  TEST_DESC_TEMPLATE.format(
214  name=name,
215  path="",
216  pytest_cmd=f"{args['coverage_command']}",
217  properties=properties + " LABELS coverage",
218  )
219  )
220  # coverage reports require all related tests to be run first
221  output.write(
222  f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
223  f"set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED {combine_test})\n"
224  )
225 
226  output.close()
227 
228 
229 def pytest_configure(config):
230  config.addinivalue_line(
231  "markers",
232  "ctest_fixture_setup(name): mark test to set up a fixture needed by another test",
233  )
234  config.addinivalue_line(
235  "markers",
236  "ctest_fixture_required(name): mark test to require a fixture set up by another test",
237  )
ReadAndWriteWhiteBoard.Path
Path
Definition: ReadAndWriteWhiteBoard.py:58
collect_for_ctest.pytest_configure
def pytest_configure(config)
Definition: collect_for_ctest.py:229
collect_for_ctest.pytest_collection_finish
def pytest_collection_finish(session)
Definition: collect_for_ctest.py:109
collect_for_ctest.pytest_collection_modifyitems
def pytest_collection_modifyitems(session, config, items)
Definition: collect_for_ctest.py:89
format
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition: MsgStream.cpp:119
GaudiPython.Pythonizations.update
update
Definition: Pythonizations.py:145
collect_for_ctest.pytest_collectstart
def pytest_collectstart(collector)
Definition: collect_for_ctest.py:73
collect_for_ctest.pytest_addoption
def pytest_addoption(parser, pluginmanager)
Definition: collect_for_ctest.py:24