Coverage for pesummary/cli/summarytest.py: 0.0%
187 statements
« prev ^ index » next coverage.py v7.4.4, created at 2026-01-15 17:49 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2026-01-15 17:49 +0000
1# Licensed under an MIT style license -- see LICENSE.md
3import subprocess
4import os
5import sys
6import pesummary
7from pesummary.core.cli.parser import ArgumentParser as _ArgumentParser
8from pesummary.core.fetch import download_dir, download_and_read_file
9from pesummary.utils.utils import logger
10from pesummary.utils.decorators import tmp_directory
11import numpy as np
12import glob
13from pathlib import Path
15__author__ = ["Charlie Hoy <charlie.hoy@ligo.org>"]
16ALLOWED = [
17 "executables", "imports", "tests", "workflow", "skymap", "bilby",
18 "bilby_pipe", "pycbc", "lalinference", "GWTC1", "GWTC2", "GWTC3", "GWTC4",
19 "examples"
20]
22PESUMMARY_DIR = Path(pesummary.__file__).parent.parent
25class ArgumentParser(_ArgumentParser):
26 def _pesummary_options(self):
27 options = super(ArgumentParser, self)._pesummary_options()
28 options.update(
29 {
30 "--type": {
31 "short": "-t",
32 "required": True,
33 "type": str,
34 "help": (
35 "The test you wish to run. Available tests are: "
36 "{}".format(", ".join(ALLOWED))
37 )
38 },
39 "--coverage": {
40 "short": "-c",
41 "default": False,
42 "action": "store_true",
43 "help": "Generare a coverage report for the testing suite"
44 },
45 "--mark": {
46 "short": "-m",
47 "default": "",
48 "type": str,
49 "help": "only run tests matching given mark expression"
50 },
51 "--ignore": {
52 "short": "-i",
53 "nargs": "+",
54 "default": [],
55 "help": "Testing scripts you wish to ignore"
56 },
57 "--expression": {
58 "short": "-k",
59 "type": str,
60 "help": (
61 "Run tests which contain names that match the given "
62 "string expression"
63 ),
64 },
65 "--pytest_config": {
66 "help": "Path to configuration file to use with pytest"
67 },
68 "--output": {
69 "default": ".",
70 "help": (
71 "Directory to store the output from the testing scripts"
72 ),
73 "short": "-o",
74 },
75 "--repository": {
76 "short": "-r",
77 "help": "Location of the pesummary repository",
78 "default": os.path.join(".", "pesummary")
79 },
80 }
81 )
82 return options
85def launch(
86 command, check_call=True, err=subprocess.DEVNULL, out=subprocess.DEVNULL
87):
88 """Launch a subprocess and run a command line
90 Parameters
91 ----------
92 command: str
93 command you wish to run
94 """
95 logger.info("Launching subprocess to run: '{}'".format(command))
96 if check_call:
97 return subprocess.check_call(command, shell=True)
98 p = subprocess.Popen(command, shell=True, stdout=out, stderr=err)
99 return p
102def executables(*args, **kwargs):
103 """Test all pesummary executables
104 """
105 command_line = (
106 "bash {}".format(
107 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "executables.sh")
108 )
109 )
110 return launch(command_line)
113def imports(*args, **kwargs):
114 """Test all pesummary imports
115 """
116 command_line = (
117 "bash {}".format(
118 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "imports.sh")
119 )
120 )
121 return launch(command_line)
124def tests(*args, output="./", multi_process=1, **kwargs):
125 """Run the pesummary testing suite
126 """
127 from pesummary.gw.fetch import fetch_open_samples
129 # download files for tests
130 logger.info(f"Downloading files for tests ({download_dir})")
131 download_path = Path(download_dir)
132 download_path.mkdir(parents=True, exist_ok=True)
133 download_and_read_file(
134 "https://dcc.ligo.org/public/0168/P2000183/008/GW190814_posterior_samples.h5",
135 outdir=download_path,
136 read_file=False,
137 download_kwargs=dict(
138 cache=True,
139 pkgname="pesummary",
140 )
141 )
142 download_and_read_file(
143 "https://dcc.ligo.org/public/0163/P190412/012/GW190412_posterior_samples_v3.h5",
144 outdir=download_path,
145 read_file=False,
146 cache=True,
147 pkgname="pesummary",
148 download_kwargs=dict(
149 cache=True,
150 pkgname="pesummary",
151 )
152 )
153 fetch_open_samples(
154 "GW190424_180648",
155 read_file=False,
156 outdir=download_dir,
157 unpack=True,
158 path="GW190424_180648.h5",
159 catalog="GWTC-2",
160 download_kwargs=dict(
161 cache=True,
162 pkgname="pesummary",
163 timeout=60,
164 )
165 )
167 # launch pytest job
168 command_line = (
169 "{} -m pytest --full-trace --verbose "
170 "--reruns 2 --pyargs pesummary.tests ".format(
171 sys.executable,
172 )
173 )
174 if multi_process > 1:
175 command_line += f"-n {multi_process} --max-worker-restart=2 "
176 if kwargs.get("pytest_config", None) is not None:
177 command_line += "-c {} ".format(kwargs.get("pytest_config"))
178 if kwargs.get("coverage", False):
179 command_line += (
180 "--cov=pesummary --cov-report html:{}/htmlcov --cov-report "
181 "term:skip-covered --cov-append ".format(output)
182 )
183 for ignore in kwargs.get("ignore", []):
184 command_line += "--ignore {} ".format(ignore)
185 if len(kwargs.get("mark", "")):
186 command_line += "-m '{}' ".format(kwargs.get("mark"))
187 if kwargs.get("expression", None) is not None:
188 command_line += "-k {} ".format(kwargs.get("expression"))
189 launch(command_line)
190 if kwargs.get("coverage", False):
191 command_line = "coverage-badge -o {} -f".format(
192 os.path.join(output, "coverage_badge.svg")
193 )
194 launch(command_line)
197@tmp_directory
198def workflow(*args, multi_process=1, **kwargs):
199 """Run the pesummary.tests.workflow_test test
200 """
201 command_line = (
202 "{} -m pytest -n {} --max-worker-restart=2 --reruns 2 --pyargs "
203 "pesummary.tests.workflow_test ".format(sys.executable, multi_process)
204 )
205 if kwargs.get("pytest_config", None) is not None:
206 command_line += "-c {} ".format(kwargs.get("pytest_config"))
207 if kwargs.get("expression", None) is not None:
208 command_line += "-k '{}' ".format(kwargs.get("expression"))
209 return launch(command_line)
212def skymap(*args, output="./", **kwargs):
213 """Run the pesummary.tests.ligo_skymap_test
214 """
215 command_line = "{} -m pytest --pyargs pesummary.tests.ligo_skymap_test ".format(
216 sys.executable
217 )
218 if kwargs.get("coverage", False):
219 command_line += (
220 "--cov=pesummary --cov-report html:{}/htmlcov --cov-report "
221 "term:skip-covered --cov-append ".format(output)
222 )
223 return launch(command_line)
226@tmp_directory
227def lalinference(*args, **kwargs):
228 """Test a lalinference run
229 """
230 command_line = "bash {}".format(
231 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "lalinference.sh")
232 )
233 return launch(command_line)
236@tmp_directory
237def dingo(*args, **kwargs):
238 """Test a dingo run"""
239 command_line = "bash {}".format(
240 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "dingo.sh")
241 )
242 return launch(command_line)
245@tmp_directory
246def bilby(*args, **kwargs):
247 """Test a bilby run
248 """
249 command_line = "bash {}".format(
250 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby.sh")
251 )
252 _ = launch(command_line)
253 command_line = "bash {}".format(
254 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby_mcmc.sh")
255 )
256 return launch(command_line)
259@tmp_directory
260def bilby_pipe(*args, **kwargs):
261 """Test a bilby_pipe run
262 """
263 command_line = "bash {}".format(
264 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby_pipe.sh")
265 )
266 return launch(command_line)
269@tmp_directory
270def pycbc(*args, **kwargs):
271 """Test a pycbc run
272 """
273 command_line = "bash {}".format(
274 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "pycbc.sh")
275 )
276 return launch(command_line)
279def _public_pesummary_result_file(event, catalog=None, unpack=True, **kwargs):
280 """Test that pesummary can load in a previously released pesummary result
281 file
282 """
283 from pesummary.gw.fetch import fetch_open_samples
285 download = fetch_open_samples(
286 event, catalog=catalog, read_file=False, delete_on_exit=False,
287 outdir="./", unpack=unpack, download_kwargs={"timeout": 120}
288 )
289 if not unpack:
290 ext = str(download).split(".")[-1]
291 else:
292 ext = "h5"
293 command_line = "{} {} -f {}.{}".format(
294 sys.executable,
295 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "existing_file.py"),
296 os.path.join(download, download) if unpack else str(download).split(
297 f".{ext}"
298 )[0], ext
299 )
300 return launch(command_line)
303def _grab_event_names_from_gwosc(webpage):
304 """Grab a list of event names from a GWOSC 'Event Portal' web page
306 Parameters
307 ----------
308 webpage: str
309 web page url that you wish to grab data from
310 """
311 from bs4 import BeautifulSoup
312 import requests
313 page = requests.get(webpage)
314 soup = BeautifulSoup(page.content, 'html.parser')
315 entries = soup.find_all("td")
316 _events = {
317 num: e.text.strip().replace(" ", "") for num, e in enumerate(entries) if
318 "GW" in e.text and "GWTC" not in e.text
319 }
320 # check that there are posterior samples available
321 events = []
322 for num, event in _events.items():
323 # check for entry in inferred primary mass column
324 if "--" not in entries[num + 4].text:
325 events.append(event)
326 return events
329@tmp_directory
330def GWTCN(
331 *args, catalog=None, size=5, include_exceptional=[], **kwargs
332):
333 """Test that pesummary can load a random selection of samples from the
334 GWTC-2 or GWTC-3 data releases
336 Parameters
337 ----------
338 catalog: str
339 name of the gravitational wave catalog you wish to consider
340 size: int, optional
341 number of events to randomly draw. Default 5
342 include_exceptional: list, optional
343 List of exceptional event candidates to include in the random selection
344 of events. This means that the total number of events could be as
345 large as size + N where N is the length of include_exceptional. Default
346 []
347 """
348 if catalog is None:
349 raise ValueError("Please provide a valid catalog")
350 events = _grab_event_names_from_gwosc(
351 "https://www.gw-openscience.org/eventapi/html/{}/".format(catalog)
352 )
353 specified = np.random.choice(events, replace=False, size=size).tolist()
354 if len(include_exceptional):
355 for event in include_exceptional:
356 if event not in specified:
357 specified.append(event)
358 for event in specified:
359 _ = _public_pesummary_result_file(event, catalog=catalog, **kwargs)
360 return
363@tmp_directory
364def GWTC2(*args, **kwargs):
365 """Test that pesummary can load a random selection of samples from the
366 GWTC-2 data release
368 Parameters
369 ----------
370 size: int, optional
371 number of events to randomly draw. Default 5
372 include_exceptional: list, optional
373 List of exceptional event candidates to include in the random selection
374 of events. This means that the total number of events could be as
375 large as size + N where N is the length of include_exceptional. Default
376 []
377 """
378 return GWTCN(
379 *args, catalog="GWTC-2", unpack=True,
380 include_exceptional=["GW190425", "GW190521"],
381 **kwargs
382 )
385@tmp_directory
386def GWTC4(*args, **kwargs):
387 """Test that pesummary can load a random selection of samples from the
388 GWTC-4 data release
390 Parameters
391 ----------
392 size: int, optional
393 number of events to randomly draw. Default 5
394 include_exceptional: list, optional
395 List of exceptional event candidates to include in the random selection
396 of events. This means that the total number of events could be as
397 large as size + N where N is the length of include_exceptional. Default
398 []
399 """
400 return GWTCN(*args, catalog="GWTC-4.0", unpack=False, **kwargs)
403@tmp_directory
404def GWTC3(*args, **kwargs):
405 """Test that pesummary can load a random selection of samples from the
406 GWTC-3 data release
408 Parameters
409 ----------
410 size: int, optional
411 number of events to randomly draw. Default 5
412 include_exceptional: list, optional
413 List of exceptional event candidates to include in the random selection
414 of events. This means that the total number of events could be as
415 large as size + N where N is the length of include_exceptional. Default
416 []
417 """
418 return GWTCN(*args, catalog="GWTC-3-confident", unpack=False, **kwargs)
421@tmp_directory
422def GWTC1(*args, **kwargs):
423 """Test that pesummary works on the GWTC1 data files
424 """
425 command_line = (
426 "curl -O https://dcc.ligo.org/public/0157/P1800370/004/GWTC-1_sample_release.tar.gz"
427 )
428 launch(command_line)
429 command_line = "tar -xf GWTC-1_sample_release.tar.gz"
430 launch(command_line)
431 command_line = "{} {} -f {} -t {}".format(
432 sys.executable,
433 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "existing_file.py"),
434 "GWTC-1_sample_release/GW150914_GWTC-1.hdf5",
435 "pesummary.gw.file.formats.GWTC1.GWTC1"
436 )
437 launch(command_line)
438 command_line = (
439 "summarypages --webdir ./GWTC1 --no_ligo_skymap --samples "
440 "GWTC-1_sample_release/GW150914_GWTC-1.hdf5 "
441 "GWTC-1_sample_release/GW170817_GWTC-1.hdf5 --path_to_samples "
442 "None IMRPhenomPv2NRT_highSpin_posterior --labels GW150914 GW170818 "
443 "--gw"
444 )
445 return launch(command_line)
448@tmp_directory
449def examples(*args, repository=os.path.join(".", "pesummary"), **kwargs):
450 """Test that the examples in the `pesummary` repository work
451 """
452 examples_dir = os.path.join(repository, "examples")
453 gw_examples = os.path.join(examples_dir, "gw")
454 core_examples = os.path.join(examples_dir, "core")
455 shell_scripts = glob.glob(os.path.join(gw_examples, "*.sh"))
456 process = {}
457 for script in shell_scripts:
458 command_line = f"bash {script}"
459 p = launch(command_line, check_call=False)
460 process[command_line] = p
461 python_scripts = glob.glob(os.path.join(gw_examples, "*.py"))
462 python_scripts += [os.path.join(core_examples, "bounded_kdeplot.py")]
463 for script in python_scripts:
464 command_line = f"python {script}"
465 p = launch(command_line, check_call=False)
466 process[command_line] = p
467 failed = []
468 while len(process):
469 _remove = []
470 for key, item in process.items():
471 if item.poll() is not None and item.returncode != 0:
472 failed.append(key)
473 elif item.poll() is not None:
474 logger.info("The following test passed: {}".format(key))
475 _remove.append(key)
476 for key in _remove:
477 process.pop(key)
478 if len(failed):
479 raise ValueError(
480 "The following tests failed: {}".format(", ".join(failed))
481 )
482 return
485def main(args=None):
486 """Top level interface for `summarytest`
487 """
488 parser = ArgumentParser()
489 parser.add_known_options_to_parser(
490 [
491 "--type", "--coverage", "--mark", "--ignore", "--expression",
492 "--pytest_config", "--output", "--repository", "--multi_process"
493 ]
494 )
495 opts, unknown = parser.parse_known_args(args=args)
496 if opts.type not in ALLOWED:
497 raise NotImplementedError(
498 "Invalid test type {}. Please choose one from the following: "
499 "{}".format(opts.type, ", ".join(ALLOWED))
500 )
501 type_mapping = {_type: eval(_type) for _type in ALLOWED}
502 try:
503 type_mapping[opts.type](
504 coverage=opts.coverage, mark=opts.mark, expression=opts.expression,
505 ignore=opts.ignore, pytest_config=opts.pytest_config,
506 output=opts.output, repository=os.path.abspath(opts.repository),
507 multi_process=opts.multi_process
508 )
509 except subprocess.CalledProcessError as e:
510 raise ValueError(
511 "The {} test failed with error {}".format(opts.type, e)
512 )
515if __name__ == "__main__":
516 main()