The Gaudi Framework  master (37c0b60a)
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  if not output_filename:
113  # nothing to do if no output file is specified
114  return
115  output = open(output_filename, "w")
116  output_rootdir = Path(output_filename).parent
117 
118  coverage = args["coverage"].split(",") if args["coverage"] else []
119  if coverage:
120  args["pytest_command"] += " --cov-report= --cov-reset " + " ".join(
121  f"--cov={module}" for module in coverage
122  )
123 
124  properties = 'LABELS "{}" '.format(";".join(args["label"]))
125  if args.get("binary_dir"):
126  properties += f'ENVIRONMENT "CMAKE_CURRENT_BINARY_DIR={args["binary_dir"]}" '
127  properties += " ".join(args["properties"])
128 
129  names = []
130  for path in sorted(session.ctest_files):
131  name = (
132  args["prefix"] + os.path.relpath(path, args["pytest_root_dir"])
133  ).replace("/", ".")
134  if name.endswith(".py"):
135  name = name[:-3]
136  pytest_cmd = args["pytest_command"]
137 
138  output.write(
139  TEST_DESC_TEMPLATE.format(
140  name=name,
141  path=path,
142  pytest_cmd=pytest_cmd,
143  properties=properties,
144  )
145  )
146 
147  if session.ctest_fixture_setup.get(path):
148  output.write(
149  'set_tests_properties('
150  f'{name} PROPERTIES FIXTURES_SETUP "{";".join(session.ctest_fixture_setup[path])}")\n'
151  )
152 
153  if session.ctest_fixture_required.get(path):
154  output.write(
155  'set_tests_properties('
156  f'{name} PROPERTIES FIXTURES_REQUIRED "{";".join(session.ctest_fixture_required[path])}")\n'
157  )
158 
159  # we force one test to be run one by one
160  if coverage:
161  # generate one coverage file per test
162  output.write(
163  f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage.{name})\n"
164  f"set_tests_properties({name} PROPERTIES FIXTURES_SETUP {name})\n"
165  )
166  names.append(name)
167 
168  if coverage and names:
169  combine_test = (args["prefix"] + "coverage_combine").replace("/", ".")
170  combine_command = re.sub(
171  r" report .*",
172  f" combine {' '.join(f'{output_rootdir}/.coverage.{n}' for n in names)}",
173  args["coverage_command"],
174  )
175  output.write(
176  TEST_DESC_TEMPLATE.format(
177  name=combine_test,
178  path="",
179  pytest_cmd=combine_command,
180  properties=properties,
181  )
182  )
183  output.write(
184  f"set_tests_properties({combine_test} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
185  f"set_tests_properties({combine_test} PROPERTIES FIXTURES_REQUIRED \"{';'.join(names)}\")\n"
186  f"set_tests_properties({combine_test} PROPERTIES FIXTURES_SETUP {combine_test})\n"
187  )
188 
189  name = (args["prefix"] + "coverage_report").replace("/", ".")
190  output.write(
191  TEST_DESC_TEMPLATE.format(
192  name=name,
193  path="",
194  pytest_cmd=f"{args['coverage_command']}",
195  properties=properties + " LABELS coverage",
196  )
197  )
198  # coverage reports require all related tests to be run first
199  output.write(
200  f"set_tests_properties({name} PROPERTIES ENVIRONMENT COVERAGE_FILE={output_rootdir}/.coverage)\n"
201  f"set_tests_properties({name} PROPERTIES FIXTURES_REQUIRED {combine_test})\n"
202  )
203 
204  output.close()
205 
206 
207 def pytest_configure(config):
208  config.addinivalue_line(
209  "markers",
210  "ctest_fixture_setup(name): mark test to set up a fixture needed by another test",
211  )
212  config.addinivalue_line(
213  "markers",
214  "ctest_fixture_required(name): mark test to require a fixture set up by another test",
215  )
ReadAndWriteWhiteBoard.Path
Path
Definition: ReadAndWriteWhiteBoard.py:58
collect_for_ctest.pytest_configure
def pytest_configure(config)
Definition: collect_for_ctest.py:207
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