Hide keyboard shortcuts

Hot-keys 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

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.log as lsstLog 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

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 intLevel = getattr(lsstLog.Log, logLevel.upper()) 

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

300 namespace = self.ap.parse_args( 

301 config=self.config, 

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

303 ) 

304 self.assertEqual(namespace.log.getLevel(), 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.getLevel(), intLevel) 

317 self.assertEqual(lsstLog.Log.getLogger("foo.bar").getLevel(), intLevel) 

318 self.assertEqual(lsstLog.Log.getLogger("baz").getLevel(), getattr(lsstLog.Log, bazLevel)) 

319 

320 with self.assertRaises(SystemExit): 

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

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

323 ) 

324 

325 with self.assertRaises(SystemExit): 

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

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

328 ) 

329 

330 def testPipeVars(self): 

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

332 """ 

333 os.environ["PIPE_INPUT_ROOT"] = DataPath 

334 namespace = self.ap.parse_args( 

335 config=self.config, 

336 args=["."], 

337 ) 

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

339 self.assertEqual(namespace.calib, None) 

340 

341 os.environ["PIPE_CALIB_ROOT"] = DataPath 

342 namespace = self.ap.parse_args( 

343 config=self.config, 

344 args=["."], 

345 ) 

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

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

348 

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

350 os.environ["PIPE_OUTPUT_ROOT"] = DataPath 

351 namespace = self.ap.parse_args( 

352 config=self.config, 

353 args=["."], 

354 ) 

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

356 self.assertEqual(namespace.calib, None) 

357 self.assertEqual(namespace.output, None) 

358 

359 def testBareHelp(self): 

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

361 """ 

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

363 try: 

364 self.ap.parse_args( 

365 config=self.config, 

366 args=[helpArg], 

367 ) 

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

369 except SystemExit as e: 

370 self.assertEqual(e.code, 0) 

371 

372 def testDatasetArgumentBasics(self): 

373 """Test DatasetArgument basics""" 

374 dsTypeHelp = "help text for dataset argument" 

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

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

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

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

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

380 self.assertEqual(dsType.help, dsTypeHelp) 

381 

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

383 namespace = ap.parse_args( 

384 config=self.config, 

385 args=[DataPath, 

386 argName, "calexp", 

387 "--id", "visit=2", 

388 ], 

389 ) 

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

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

392 

393 del namespace 

394 

395 if default is None: 

396 # make sure dataset type argument is required 

397 with self.assertRaises(SystemExit): 

398 ap.parse_args( 

399 config=self.config, 

400 args=[DataPath, 

401 "--id", "visit=2", 

402 ], 

403 ) 

404 else: 

405 namespace = ap.parse_args( 

406 config=self.config, 

407 args=[DataPath, 

408 "--id", "visit=2", 

409 ], 

410 ) 

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

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

413 

414 def testDatasetArgumentPositional(self): 

415 """Test DatasetArgument with a positional argument""" 

416 name = "foo" 

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

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

419 dsType = pipeBase.DatasetArgument(name=name) 

420 self.assertEqual(dsType.help, defaultDsTypeHelp) 

421 

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

423 namespace = ap.parse_args( 

424 config=self.config, 

425 args=[DataPath, 

426 "calexp", 

427 "--id", "visit=2", 

428 ], 

429 ) 

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

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

432 

433 # make sure dataset type argument is required 

434 with self.assertRaises(SystemExit): 

435 ap.parse_args( 

436 config=self.config, 

437 args=[DataPath, 

438 "--id", "visit=2", 

439 ], 

440 ) 

441 

442 def testConfigDatasetTypeFieldDefault(self): 

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

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

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

446 # so the test can tell the difference 

447 name = "dsType" 

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

449 dsType = pipeBase.ConfigDatasetType(name=name) 

450 

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

452 namespace = ap.parse_args( 

453 config=self.config, 

454 args=[DataPath, 

455 "--id", "visit=2", 

456 ], 

457 ) 

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

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

460 

461 def testConfigDatasetTypeNoFieldDefault(self): 

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

463 name = "dsTypeNoDefault" 

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

465 dsType = pipeBase.ConfigDatasetType(name=name) 

466 

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

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

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

470 # and post-process the ID list) 

471 with self.assertRaises(RuntimeError): 

472 ap.parse_args( 

473 config=self.config, 

474 args=[DataPath, 

475 "--id", "visit=2", 

476 ], 

477 ) 

478 

479 def testOutputs(self): 

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

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

482 self.assertTrue(parser.requireOutput) 

483 

484 # Location of our working repository 

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

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

487 repositoryPath = tempfile.mkdtemp() 

488 try: 

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

490 # output repository at repositoryPath 

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

492 self.assertEqual(args.input, DataPath) 

493 self.assertEqual(args.output, repositoryPath) 

494 self.assertIsNone(args.rerun) 

495 

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

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

498 self.assertEqual(args.input, repositoryPath) 

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

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

501 

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

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

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

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

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

507 

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

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

510 rerun = os.path.basename(rerunPath) 

511 try: 

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

513 self.assertEqual(args.input, repositoryPath) 

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

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

516 finally: 

517 shutil.rmtree(rerunPath) 

518 

519 finally: 

520 shutil.rmtree(repositoryPath) 

521 

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

523 with self.assertRaises(SystemExit): 

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

525 

526 def testReuseOption(self): 

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

528 namespace = self.ap.parse_args( 

529 config=self.config, 

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

531 ) 

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

533 namespace = self.ap.parse_args( 

534 config=self.config, 

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

536 ) 

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

538 namespace = self.ap.parse_args( 

539 config=self.config, 

540 args=[DataPath], 

541 ) 

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

543 

544 

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

546 pass 

547 

548 

549def setup_module(module): 

550 lsst.utils.tests.init() 

551 

552 

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

554 lsst.utils.tests.init() 

555 unittest.main()