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

1# Licensed under an MIT style license -- see LICENSE.md 

2 

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 

14 

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] 

21 

22PESUMMARY_DIR = Path(pesummary.__file__).parent.parent 

23 

24 

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 

83 

84 

85def launch( 

86 command, check_call=True, err=subprocess.DEVNULL, out=subprocess.DEVNULL 

87): 

88 """Launch a subprocess and run a command line 

89 

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 

100 

101 

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) 

111 

112 

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) 

122 

123 

124def tests(*args, output="./", multi_process=1, **kwargs): 

125 """Run the pesummary testing suite 

126 """ 

127 from pesummary.gw.fetch import fetch_open_samples 

128 

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 ) 

166 

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) 

195 

196 

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) 

210 

211 

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) 

224 

225 

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) 

234 

235 

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) 

243 

244 

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) 

257 

258 

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) 

267 

268 

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) 

277 

278 

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 

284 

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) 

301 

302 

303def _grab_event_names_from_gwosc(webpage): 

304 """Grab a list of event names from a GWOSC 'Event Portal' web page 

305 

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 

327 

328 

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 

335 

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 

361 

362 

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 

367 

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 ) 

383 

384 

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 

389 

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) 

401 

402 

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 

407 

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) 

419 

420 

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) 

446 

447 

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 

483 

484 

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 ) 

513 

514 

515if __name__ == "__main__": 

516 main()