Coverage for pesummary/cli/summarytest.py: 0.0%
183 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-11-05 13:38 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-11-05 13:38 +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 bilby(*args, **kwargs):
238 """Test a bilby run
239 """
240 command_line = "bash {}".format(
241 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby.sh")
242 )
243 _ = launch(command_line)
244 command_line = "bash {}".format(
245 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby_mcmc.sh")
246 )
247 return launch(command_line)
250@tmp_directory
251def bilby_pipe(*args, **kwargs):
252 """Test a bilby_pipe run
253 """
254 command_line = "bash {}".format(
255 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "bilby_pipe.sh")
256 )
257 return launch(command_line)
260@tmp_directory
261def pycbc(*args, **kwargs):
262 """Test a pycbc run
263 """
264 command_line = "bash {}".format(
265 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "pycbc.sh")
266 )
267 return launch(command_line)
270def _public_pesummary_result_file(event, catalog=None, unpack=True, **kwargs):
271 """Test that pesummary can load in a previously released pesummary result
272 file
273 """
274 from pesummary.gw.fetch import fetch_open_samples
276 download = fetch_open_samples(
277 event, catalog=catalog, read_file=False, delete_on_exit=False,
278 outdir="./", unpack=unpack, download_kwargs={"timeout": 120}
279 )
280 if not unpack:
281 ext = str(download).split(".")[-1]
282 else:
283 ext = "h5"
284 command_line = "{} {} -f {}.{}".format(
285 sys.executable,
286 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "existing_file.py"),
287 os.path.join(download, download) if unpack else str(download).split(
288 f".{ext}"
289 )[0], ext
290 )
291 return launch(command_line)
294def _grab_event_names_from_gwosc(webpage):
295 """Grab a list of event names from a GWOSC 'Event Portal' web page
297 Parameters
298 ----------
299 webpage: str
300 web page url that you wish to grab data from
301 """
302 from bs4 import BeautifulSoup
303 import requests
304 page = requests.get(webpage)
305 soup = BeautifulSoup(page.content, 'html.parser')
306 entries = soup.find_all("td")
307 _events = {
308 num: e.text.strip().replace(" ", "") for num, e in enumerate(entries) if
309 "GW" in e.text and "GWTC" not in e.text
310 }
311 # check that there are posterior samples available
312 events = []
313 for num, event in _events.items():
314 # check for entry in inferred primary mass column
315 if "--" not in entries[num + 4].text:
316 events.append(event)
317 return events
320@tmp_directory
321def GWTCN(
322 *args, catalog=None, size=5, include_exceptional=[], **kwargs
323):
324 """Test that pesummary can load a random selection of samples from the
325 GWTC-2 or GWTC-3 data releases
327 Parameters
328 ----------
329 catalog: str
330 name of the gravitational wave catalog you wish to consider
331 size: int, optional
332 number of events to randomly draw. Default 5
333 include_exceptional: list, optional
334 List of exceptional event candidates to include in the random selection
335 of events. This means that the total number of events could be as
336 large as size + N where N is the length of include_exceptional. Default
337 []
338 """
339 if catalog is None:
340 raise ValueError("Please provide a valid catalog")
341 events = _grab_event_names_from_gwosc(
342 "https://www.gw-openscience.org/eventapi/html/{}/".format(catalog)
343 )
344 specified = np.random.choice(events, replace=False, size=size).tolist()
345 if len(include_exceptional):
346 for event in include_exceptional:
347 if event not in specified:
348 specified.append(event)
349 for event in specified:
350 _ = _public_pesummary_result_file(event, catalog=catalog, **kwargs)
351 return
354@tmp_directory
355def GWTC2(*args, **kwargs):
356 """Test that pesummary can load a random selection of samples from the
357 GWTC-2 data release
359 Parameters
360 ----------
361 size: int, optional
362 number of events to randomly draw. Default 5
363 include_exceptional: list, optional
364 List of exceptional event candidates to include in the random selection
365 of events. This means that the total number of events could be as
366 large as size + N where N is the length of include_exceptional. Default
367 []
368 """
369 return GWTCN(
370 *args, catalog="GWTC-2", unpack=True,
371 include_exceptional=["GW190425", "GW190521"],
372 **kwargs
373 )
376@tmp_directory
377def GWTC4(*args, **kwargs):
378 """Test that pesummary can load a random selection of samples from the
379 GWTC-4 data release
381 Parameters
382 ----------
383 size: int, optional
384 number of events to randomly draw. Default 5
385 include_exceptional: list, optional
386 List of exceptional event candidates to include in the random selection
387 of events. This means that the total number of events could be as
388 large as size + N where N is the length of include_exceptional. Default
389 []
390 """
391 return GWTCN(*args, catalog="GWTC-4.0", unpack=False, **kwargs)
394@tmp_directory
395def GWTC3(*args, **kwargs):
396 """Test that pesummary can load a random selection of samples from the
397 GWTC-3 data release
399 Parameters
400 ----------
401 size: int, optional
402 number of events to randomly draw. Default 5
403 include_exceptional: list, optional
404 List of exceptional event candidates to include in the random selection
405 of events. This means that the total number of events could be as
406 large as size + N where N is the length of include_exceptional. Default
407 []
408 """
409 return GWTCN(*args, catalog="GWTC-3-confident", unpack=False, **kwargs)
412@tmp_directory
413def GWTC1(*args, **kwargs):
414 """Test that pesummary works on the GWTC1 data files
415 """
416 command_line = (
417 "curl -O https://dcc.ligo.org/public/0157/P1800370/004/GWTC-1_sample_release.tar.gz"
418 )
419 launch(command_line)
420 command_line = "tar -xf GWTC-1_sample_release.tar.gz"
421 launch(command_line)
422 command_line = "{} {} -f {} -t {}".format(
423 sys.executable,
424 os.path.join(PESUMMARY_DIR, "pesummary", "tests", "existing_file.py"),
425 "GWTC-1_sample_release/GW150914_GWTC-1.hdf5",
426 "pesummary.gw.file.formats.GWTC1.GWTC1"
427 )
428 launch(command_line)
429 command_line = (
430 "summarypages --webdir ./GWTC1 --no_ligo_skymap --samples "
431 "GWTC-1_sample_release/GW150914_GWTC-1.hdf5 "
432 "GWTC-1_sample_release/GW170817_GWTC-1.hdf5 --path_to_samples "
433 "None IMRPhenomPv2NRT_highSpin_posterior --labels GW150914 GW170818 "
434 "--gw"
435 )
436 return launch(command_line)
439@tmp_directory
440def examples(*args, repository=os.path.join(".", "pesummary"), **kwargs):
441 """Test that the examples in the `pesummary` repository work
442 """
443 examples_dir = os.path.join(repository, "examples")
444 gw_examples = os.path.join(examples_dir, "gw")
445 core_examples = os.path.join(examples_dir, "core")
446 shell_scripts = glob.glob(os.path.join(gw_examples, "*.sh"))
447 process = {}
448 for script in shell_scripts:
449 command_line = f"bash {script}"
450 p = launch(command_line, check_call=False)
451 process[command_line] = p
452 python_scripts = glob.glob(os.path.join(gw_examples, "*.py"))
453 python_scripts += [os.path.join(core_examples, "bounded_kdeplot.py")]
454 for script in python_scripts:
455 command_line = f"python {script}"
456 p = launch(command_line, check_call=False)
457 process[command_line] = p
458 failed = []
459 while len(process):
460 _remove = []
461 for key, item in process.items():
462 if item.poll() is not None and item.returncode != 0:
463 failed.append(key)
464 elif item.poll() is not None:
465 logger.info("The following test passed: {}".format(key))
466 _remove.append(key)
467 for key in _remove:
468 process.pop(key)
469 if len(failed):
470 raise ValueError(
471 "The following tests failed: {}".format(", ".join(failed))
472 )
473 return
476def main(args=None):
477 """Top level interface for `summarytest`
478 """
479 parser = ArgumentParser()
480 parser.add_known_options_to_parser(
481 [
482 "--type", "--coverage", "--mark", "--ignore", "--expression",
483 "--pytest_config", "--output", "--repository", "--multi_process"
484 ]
485 )
486 opts, unknown = parser.parse_known_args(args=args)
487 if opts.type not in ALLOWED:
488 raise NotImplementedError(
489 "Invalid test type {}. Please choose one from the following: "
490 "{}".format(opts.type, ", ".join(ALLOWED))
491 )
492 type_mapping = {_type: eval(_type) for _type in ALLOWED}
493 try:
494 type_mapping[opts.type](
495 coverage=opts.coverage, mark=opts.mark, expression=opts.expression,
496 ignore=opts.ignore, pytest_config=opts.pytest_config,
497 output=opts.output, repository=os.path.abspath(opts.repository),
498 multi_process=opts.multi_process
499 )
500 except subprocess.CalledProcessError as e:
501 raise ValueError(
502 "The {} test failed with error {}".format(opts.type, e)
503 )
506if __name__ == "__main__":
507 main()