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

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 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) 

248 

249 

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) 

258 

259 

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) 

268 

269 

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 

275 

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) 

292 

293 

294def _grab_event_names_from_gwosc(webpage): 

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

296 

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 

318 

319 

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 

326 

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 

352 

353 

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 

358 

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 ) 

374 

375 

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 

380 

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) 

392 

393 

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 

398 

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) 

410 

411 

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) 

437 

438 

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 

474 

475 

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 ) 

504 

505 

506if __name__ == "__main__": 

507 main()