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.pipe.base.task_logging import getTaskLogger 

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(getTaskLogger("foo.bar").getEffectiveLevel(), intLevel) 

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

319 getattr(getTaskLogger(), 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()