Coverage for tests/test_argumentParser.py: 18%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

276 statements  

1# 

2# LSST Data Management System 

3# Copyright 2008-2015 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22import itertools 

23import os 

24import shutil 

25import tempfile 

26import unittest 

27import contextlib 

28 

29import lsst.utils 

30import lsst.utils.tests 

31import lsst.pex.config as pexConfig 

32import lsst.pipe.base as pipeBase 

33from lsst.utils.logging import getLogger 

34 

35ObsTestDir = lsst.utils.getPackageDir("obs_test") 

36DataPath = os.path.realpath(os.path.join(ObsTestDir, "data", "input")) 

37LocalDataPath = os.path.join(os.path.dirname(__file__), "data") 

38 

39 

40@contextlib.contextmanager 

41def capture(): 

42 """Context manager to intercept stdout/err 

43 

44 Use as: 

45 with capture() as out: 

46 print 'hi' 

47 

48 http://stackoverflow.com/questions/5136611/capture-stdout-from-a-script-in-python 

49 """ 

50 import sys 

51 from io import StringIO 

52 oldout, olderr = sys.stdout, sys.stderr 

53 try: 

54 out = [StringIO(), StringIO()] 

55 sys.stdout, sys.stderr = out 

56 yield out 

57 finally: 

58 sys.stdout, sys.stderr = oldout, olderr 

59 out[0] = out[0].getvalue() 

60 out[1] = out[1].getvalue() 

61 

62 

63class SubConfig(pexConfig.Config): 

64 intItem = pexConfig.Field(doc="sample int field", dtype=int, default=8) 

65 

66 

67class SampleConfig(pexConfig.Config): 

68 boolItem = pexConfig.Field(doc="sample bool field", dtype=bool, default=True) 

69 floatItem = pexConfig.Field(doc="sample float field", dtype=float, default=3.1) 

70 strItem = pexConfig.Field(doc="sample str field", dtype=str, default="strDefault") 

71 subItem = pexConfig.ConfigField(doc="sample subfield", dtype=SubConfig) 

72 multiDocItem = pexConfig.Field(doc="1. sample... \n#2...multiline \n##3...#\n###4...docstring", 

73 dtype=str, default="multiLineDoc") 

74 dsType = pexConfig.Field(doc="dataset type for --id argument", dtype=str, default="calexp") 

75 dsTypeNoDefault = pexConfig.Field(doc="dataset type for --id argument; no default", dtype=str, 

76 optional=True) 

77 

78 

79class ArgumentParserTestCase(unittest.TestCase): 

80 """A test case for ArgumentParser.""" 

81 

82 def setUp(self): 

83 self.ap = pipeBase.InputOnlyArgumentParser(name="argumentParser") 

84 self.ap.add_id_argument("--id", "raw", "help text") 

85 self.ap.add_id_argument("--otherId", "raw", "more help") 

86 self.config = SampleConfig() 

87 os.environ.pop("PIPE_INPUT_ROOT", None) 

88 os.environ.pop("PIPE_CALIB_ROOT", None) 

89 os.environ.pop("PIPE_OUTPUT_ROOT", None) 

90 

91 def tearDown(self): 

92 del self.ap 

93 del self.config 

94 

95 def testBasicId(self): 

96 """Test --id basics""" 

97 namespace = self.ap.parse_args( 

98 config=self.config, 

99 args=[DataPath, "--id", "visit=1", "filter=g"], 

100 ) 

101 self.assertEqual(len(namespace.id.idList), 1) 

102 self.assertEqual(len(namespace.id.refList), 1) 

103 

104 namespace = self.ap.parse_args( 

105 config=self.config, 

106 args=[DataPath, "--id", "visit=22", "filter=g"], 

107 ) 

108 self.assertEqual(len(namespace.id.idList), 1) 

109 self.assertEqual(len(namespace.id.refList), 0) # no data for this ID 

110 

111 def testOtherId(self): 

112 """Test --other""" 

113 # By itself 

114 namespace = self.ap.parse_args( 

115 config=self.config, 

116 args=[DataPath, "--other", "visit=99"], 

117 ) 

118 self.assertEqual(len(namespace.otherId.idList), 1) 

119 self.assertEqual(len(namespace.otherId.refList), 0) 

120 

121 # And together 

122 namespace = self.ap.parse_args( 

123 config=self.config, 

124 args=[DataPath, "--id", "visit=1", 

125 "--other", "visit=99"], 

126 ) 

127 self.assertEqual(len(namespace.id.idList), 1) 

128 self.assertEqual(len(namespace.id.refList), 1) 

129 self.assertEqual(len(namespace.otherId.idList), 1) 

130 self.assertEqual(len(namespace.otherId.refList), 0) # no data for this ID 

131 

132 def testIdCross(self): 

133 """Test --id cross product, including order""" 

134 visitList = (1, 2, 3) 

135 filterList = ("g", "r") 

136 namespace = self.ap.parse_args( 

137 config=self.config, 

138 args=[ 

139 DataPath, 

140 "--id", 

141 "filter=%s" % ("^".join(filterList),), 

142 "visit=%s" % ("^".join(str(visit) for visit in visitList),), 

143 ] 

144 ) 

145 self.assertEqual(len(namespace.id.idList), 6) 

146 predValList = itertools.product(filterList, visitList) 

147 for id, predVal in zip(namespace.id.idList, predValList): 

148 idVal = tuple(id[key] for key in ("filter", "visit")) 

149 self.assertEqual(idVal, predVal) 

150 self.assertEqual(len(namespace.id.refList), 3) # only have data for three of these 

151 

152 def testIdDuplicate(self): 

153 """Verify that each ID name can only appear once in a given ID argument""" 

154 with self.assertRaises(SystemExit): 

155 self.ap.parse_args(config=self.config, 

156 args=[DataPath, "--id", "visit=1", "visit=2"], 

157 ) 

158 

159 def testConfigBasics(self): 

160 """Test --config""" 

161 namespace = self.ap.parse_args( 

162 config=self.config, 

163 args=[DataPath, "--config", "boolItem=False", "floatItem=-67.1", 

164 "strItem=overridden value", "subItem.intItem=5", "multiDocItem=edited value"], 

165 ) 

166 self.assertEqual(namespace.config.boolItem, False) 

167 self.assertEqual(namespace.config.floatItem, -67.1) 

168 self.assertEqual(namespace.config.strItem, "overridden value") 

169 self.assertEqual(namespace.config.subItem.intItem, 5) 

170 self.assertEqual(namespace.config.multiDocItem, "edited value") 

171 

172 def testConfigLeftToRight(self): 

173 """Verify that order of overriding config values is left to right""" 

174 namespace = self.ap.parse_args( 

175 config=self.config, 

176 args=[DataPath, 

177 "--config", "floatItem=-67.1", "strItem=overridden value", 

178 "--config", "strItem=final value"], 

179 ) 

180 self.assertEqual(namespace.config.floatItem, -67.1) 

181 self.assertEqual(namespace.config.strItem, "final value") 

182 

183 def testConfigWrongNames(self): 

184 """Verify that incorrect names for config fields are caught""" 

185 with self.assertRaises(SystemExit): 

186 self.ap.parse_args(config=self.config, 

187 args=[DataPath, "--config", "missingItem=-67.1"], 

188 ) 

189 

190 def testShow(self): 

191 """Test --show""" 

192 with capture() as out: 

193 self.ap.parse_args( 

194 config=self.config, 

195 args=[DataPath, "--show", "config", "data", "tasks", "run"], 

196 ) 

197 res = out[0] 

198 self.assertIn("config.floatItem", res) 

199 self.assertIn("config.subItem", res) 

200 self.assertIn("config.boolItem", res) 

201 self.assertIn("config.strItem", res) 

202 self.assertIn("config.multiDocItem", res) 

203 # Test show with exact config name and with one sided, embedded, and two sided globs 

204 for configStr in ("config.multiDocItem", "*ultiDocItem", "*ulti*Item", "*ultiDocI*"): 

205 with capture() as out: 

206 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"]) 

207 res = out[0] 

208 self.assertIn("config.multiDocItem", res) 

209 

210 with self.assertRaises(SystemExit): 

211 self.ap.parse_args(config=self.config, 

212 args=[DataPath, "--show", "config"], 

213 ) 

214 with self.assertRaises(SystemExit): 

215 self.ap.parse_args(config=self.config, 

216 args=[DataPath, "--show", "config=X"], 

217 ) 

218 

219 # Test show with glob for single and multi-line doc strings 

220 for configStr, assertStr in ("*strItem*", "strDefault"), ("*multiDocItem", "multiLineDoc"): 

221 with capture() as out: 

222 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"]) 

223 stdout = out[0] 

224 stdoutList = stdout.rstrip().split("\n") 

225 self.assertGreater(len(stdoutList), 2) # at least 2 docstring lines (1st line is always a \n) 

226 # and 1 config parameter 

227 answer = [ans for ans in stdoutList if not ans.startswith("#")] # get rid of comment lines 

228 answer = [ans for ans in answer if ":NOIGNORECASE to prevent this" not in ans] 

229 self.assertEqual(len(answer), 2) # only 1 config item matches (+ 1 \n entry per doc string) 

230 self.assertIn(assertStr, answer[1]) 

231 

232 with self.assertRaises(SystemExit): 

233 self.ap.parse_args(config=self.config, 

234 args=[DataPath, "--show", "badname", "run"], 

235 ) 

236 with self.assertRaises(SystemExit): 

237 self.ap.parse_args(config=self.config, 

238 args=[DataPath, "--show"], # no --show arguments 

239 ) 

240 

241 # Test show with and without case sensitivity 

242 for configStr, shouldMatch in [("*multiDocItem*", True), 

243 ("*multidocitem*", True), 

244 ("*multidocitem*:NOIGNORECASE", False)]: 

245 with capture() as out: 

246 self.ap.parse_args(self.config, [DataPath, "--show", "config=" + configStr, "run"]) 

247 res = out[0] 

248 

249 if shouldMatch: 

250 self.assertIn("config.multiDocItem", res) 

251 else: 

252 self.assertNotIn("config.multiDocItem", res) 

253 

254 def testConfigFileBasics(self): 

255 """Test --configfile""" 

256 configFilePath = os.path.join(LocalDataPath, "argumentParserConfig.py") 

257 namespace = self.ap.parse_args( 

258 config=self.config, 

259 args=[DataPath, "--configfile", configFilePath], 

260 ) 

261 self.assertEqual(namespace.config.floatItem, -9e9) 

262 self.assertEqual(namespace.config.strItem, "set in override file") 

263 

264 def testConfigFileLeftRight(self): 

265 """Verify that order of setting values is with a mix of config file and config is left to right""" 

266 configFilePath = os.path.join(LocalDataPath, "argumentParserConfig.py") 

267 namespace = self.ap.parse_args( 

268 config=self.config, 

269 args=[DataPath, 

270 "--config", "floatItem=5.5", 

271 "--configfile", configFilePath, 

272 "--config", "strItem=value from cmd line"], 

273 ) 

274 self.assertEqual(namespace.config.floatItem, -9e9) 

275 self.assertEqual(namespace.config.strItem, "value from cmd line") 

276 

277 def testConfigFileMissingFiles(self): 

278 """Verify that missing config override files are caught""" 

279 with self.assertRaises(SystemExit): 

280 self.ap.parse_args(config=self.config, 

281 args=[DataPath, "--configfile", "missingFile"], 

282 ) 

283 

284 def testAtFile(self): 

285 """Test @file""" 

286 argPath = os.path.join(LocalDataPath, "args.txt") 

287 namespace = self.ap.parse_args( 

288 config=self.config, 

289 args=[DataPath, "@%s" % (argPath,)], 

290 ) 

291 self.assertEqual(len(namespace.id.idList), 1) 

292 self.assertEqual(namespace.config.floatItem, 4.7) 

293 self.assertEqual(namespace.config.strItem, "new value") 

294 

295 def testLogLevel(self): 

296 """Test --loglevel""" 

297 for logLevel in ("trace", "debug", "Info", "WARN", "eRRoR", "fatal"): 

298 namespace = self.ap.parse_args( 

299 config=self.config, 

300 args=[DataPath, "--loglevel", logLevel], 

301 ) 

302 intLevel = getattr(namespace.log, logLevel.upper()) 

303 print("testing logLevel=%r" % (logLevel,)) 

304 self.assertEqual(namespace.log.getEffectiveLevel(), intLevel) 

305 self.assertFalse(hasattr(namespace, "loglevel")) 

306 

307 bazLevel = "TRACE" 

308 namespace = self.ap.parse_args( 

309 config=self.config, 

310 args=[DataPath, "--loglevel", logLevel, 

311 "foo.bar=%s" % (logLevel,), 

312 "baz=INFO", 

313 "baz=%s" % bazLevel, # test that later values override earlier values 

314 ], 

315 ) 

316 self.assertEqual(namespace.log.getEffectiveLevel(), intLevel) 

317 self.assertEqual(getLogger("foo.bar").getEffectiveLevel(), intLevel) 

318 self.assertEqual(getLogger("baz").getEffectiveLevel(), 

319 getattr(getLogger(), bazLevel)) 

320 

321 with self.assertRaises(SystemExit): 

322 self.ap.parse_args(config=self.config, 

323 args=[DataPath, "--loglevel", "1234"], 

324 ) 

325 

326 with self.assertRaises(SystemExit): 

327 self.ap.parse_args(config=self.config, 

328 args=[DataPath, "--loglevel", "INVALID_LEVEL"], 

329 ) 

330 

331 def testPipeVars(self): 

332 """Test handling of $PIPE_x_ROOT environment variables, where x is INPUT, CALIB or OUTPUT 

333 """ 

334 os.environ["PIPE_INPUT_ROOT"] = DataPath 

335 namespace = self.ap.parse_args( 

336 config=self.config, 

337 args=["."], 

338 ) 

339 self.assertEqual(namespace.input, os.path.abspath(DataPath)) 

340 self.assertEqual(namespace.calib, None) 

341 

342 os.environ["PIPE_CALIB_ROOT"] = DataPath 

343 namespace = self.ap.parse_args( 

344 config=self.config, 

345 args=["."], 

346 ) 

347 self.assertEqual(namespace.input, os.path.abspath(DataPath)) 

348 self.assertEqual(namespace.calib, os.path.abspath(DataPath)) 

349 

350 os.environ.pop("PIPE_CALIB_ROOT", None) 

351 os.environ["PIPE_OUTPUT_ROOT"] = DataPath 

352 namespace = self.ap.parse_args( 

353 config=self.config, 

354 args=["."], 

355 ) 

356 self.assertEqual(namespace.input, os.path.abspath(DataPath)) 

357 self.assertEqual(namespace.calib, None) 

358 self.assertEqual(namespace.output, None) 

359 

360 def testBareHelp(self): 

361 """Make sure bare help does not print an error message (ticket #3090) 

362 """ 

363 for helpArg in ("-h", "--help"): 

364 try: 

365 self.ap.parse_args( 

366 config=self.config, 

367 args=[helpArg], 

368 ) 

369 self.fail("should have raised SystemExit") 

370 except SystemExit as e: 

371 self.assertEqual(e.code, 0) 

372 

373 def testDatasetArgumentBasics(self): 

374 """Test DatasetArgument basics""" 

375 dsTypeHelp = "help text for dataset argument" 

376 for name in (None, "--foo"): 

377 for default in (None, "raw"): 

378 argName = name if name is not None else "--id_dstype" 

379 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser") 

380 dsType = pipeBase.DatasetArgument(name=name, help=dsTypeHelp, default=default) 

381 self.assertEqual(dsType.help, dsTypeHelp) 

382 

383 ap.add_id_argument("--id", dsType, "help text") 

384 namespace = ap.parse_args( 

385 config=self.config, 

386 args=[DataPath, 

387 argName, "calexp", 

388 "--id", "visit=2", 

389 ], 

390 ) 

391 self.assertEqual(namespace.id.datasetType, "calexp") 

392 self.assertEqual(len(namespace.id.idList), 1) 

393 

394 del namespace 

395 

396 if default is None: 

397 # make sure dataset type argument is required 

398 with self.assertRaises(SystemExit): 

399 ap.parse_args( 

400 config=self.config, 

401 args=[DataPath, 

402 "--id", "visit=2", 

403 ], 

404 ) 

405 else: 

406 namespace = ap.parse_args( 

407 config=self.config, 

408 args=[DataPath, 

409 "--id", "visit=2", 

410 ], 

411 ) 

412 self.assertEqual(namespace.id.datasetType, default) 

413 self.assertEqual(len(namespace.id.idList), 1) 

414 

415 def testDatasetArgumentPositional(self): 

416 """Test DatasetArgument with a positional argument""" 

417 name = "foo" 

418 defaultDsTypeHelp = "dataset type to process from input data repository" 

419 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser") 

420 dsType = pipeBase.DatasetArgument(name=name) 

421 self.assertEqual(dsType.help, defaultDsTypeHelp) 

422 

423 ap.add_id_argument("--id", dsType, "help text") 

424 namespace = ap.parse_args( 

425 config=self.config, 

426 args=[DataPath, 

427 "calexp", 

428 "--id", "visit=2", 

429 ], 

430 ) 

431 self.assertEqual(namespace.id.datasetType, "calexp") 

432 self.assertEqual(len(namespace.id.idList), 1) 

433 

434 # make sure dataset type argument is required 

435 with self.assertRaises(SystemExit): 

436 ap.parse_args( 

437 config=self.config, 

438 args=[DataPath, 

439 "--id", "visit=2", 

440 ], 

441 ) 

442 

443 def testConfigDatasetTypeFieldDefault(self): 

444 """Test ConfigDatasetType with a config field that has a default value""" 

445 # default value for config field "dsType" is "calexp"; 

446 # use a different value as the default for the ConfigDatasetType 

447 # so the test can tell the difference 

448 name = "dsType" 

449 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser") 

450 dsType = pipeBase.ConfigDatasetType(name=name) 

451 

452 ap.add_id_argument("--id", dsType, "help text") 

453 namespace = ap.parse_args( 

454 config=self.config, 

455 args=[DataPath, 

456 "--id", "visit=2", 

457 ], 

458 ) 

459 self.assertEqual(namespace.id.datasetType, "calexp") # default of config field dsType 

460 self.assertEqual(len(namespace.id.idList), 1) 

461 

462 def testConfigDatasetTypeNoFieldDefault(self): 

463 """Test ConfigDatasetType with a config field that has no default value""" 

464 name = "dsTypeNoDefault" 

465 ap = pipeBase.InputOnlyArgumentParser(name="argumentParser") 

466 dsType = pipeBase.ConfigDatasetType(name=name) 

467 

468 ap.add_id_argument("--id", dsType, "help text") 

469 # neither the argument nor the config field has a default, 

470 # so the user must specify the argument (or specify doMakeDataRefList=False 

471 # and post-process the ID list) 

472 with self.assertRaises(RuntimeError): 

473 ap.parse_args( 

474 config=self.config, 

475 args=[DataPath, 

476 "--id", "visit=2", 

477 ], 

478 ) 

479 

480 def testOutputs(self): 

481 """Test output directories, specified in different ways""" 

482 parser = pipeBase.ArgumentParser(name="argumentParser") 

483 self.assertTrue(parser.requireOutput) 

484 

485 # Location of our working repository 

486 # We'll start by creating this, then use it as the basis for further tests 

487 # It's removed at the end of the try/finally block below 

488 repositoryPath = tempfile.mkdtemp() 

489 try: 

490 # Given input at DataPath, demonstrate that we can create a new 

491 # output repository at repositoryPath 

492 args = parser.parse_args(config=self.config, args=[DataPath, "--output", repositoryPath]) 

493 self.assertEqual(args.input, DataPath) 

494 self.assertEqual(args.output, repositoryPath) 

495 self.assertIsNone(args.rerun) 

496 

497 # Now based on our new output repository, demonstrate that we can create a rerun at rerun/foo 

498 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", "foo"]) 

499 self.assertEqual(args.input, repositoryPath) 

500 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", "foo")) 

501 self.assertEqual(args.rerun, ["foo"]) 

502 

503 # Now check that that we can chain the above rerun into another 

504 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", "foo:bar"]) 

505 self.assertEqual(args.input, os.path.join(repositoryPath, "rerun", "foo")) 

506 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", "bar")) 

507 self.assertEqual(args.rerun, ["foo", "bar"]) 

508 

509 # Finally, check that the above also works if the rerun directory already exists 

510 rerunPath = tempfile.mkdtemp(dir=os.path.join(repositoryPath, "rerun")) 

511 rerun = os.path.basename(rerunPath) 

512 try: 

513 args = parser.parse_args(config=self.config, args=[repositoryPath, "--rerun", rerun]) 

514 self.assertEqual(args.input, repositoryPath) 

515 self.assertEqual(args.output, os.path.join(repositoryPath, "rerun", rerun)) 

516 self.assertEqual(args.rerun, [rerun]) 

517 finally: 

518 shutil.rmtree(rerunPath) 

519 

520 finally: 

521 shutil.rmtree(repositoryPath) 

522 

523 # Finally, check that we raise an appropriate error if we don't specify an output location at all 

524 with self.assertRaises(SystemExit): 

525 parser.parse_args(config=self.config, args=[DataPath, ]) 

526 

527 def testReuseOption(self): 

528 self.ap.addReuseOption(["a", "b", "c"]) 

529 namespace = self.ap.parse_args( 

530 config=self.config, 

531 args=[DataPath, "--reuse-outputs-from", "b"], 

532 ) 

533 self.assertEqual(namespace.reuse, ["a", "b"]) 

534 namespace = self.ap.parse_args( 

535 config=self.config, 

536 args=[DataPath, "--reuse-outputs-from", "all"], 

537 ) 

538 self.assertEqual(namespace.reuse, ["a", "b", "c"]) 

539 namespace = self.ap.parse_args( 

540 config=self.config, 

541 args=[DataPath], 

542 ) 

543 self.assertEqual(namespace.reuse, []) 

544 

545 

546class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

547 pass 

548 

549 

550def setup_module(module): 

551 lsst.utils.tests.init() 

552 

553 

554if __name__ == "__main__": 554 ↛ 555line 554 didn't jump to line 555, because the condition on line 554 was never true

555 lsst.utils.tests.init() 

556 unittest.main()