Coverage for tests/test_argumentParser.py : 18%

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
29import lsst.utils
30import lsst.utils.tests
31import lsst.log as lsstLog
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
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")
40@contextlib.contextmanager
41def capture():
42 """Context manager to intercept stdout/err
44 Use as:
45 with capture() as out:
46 print 'hi'
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()
63class SubConfig(pexConfig.Config):
64 intItem = pexConfig.Field(doc="sample int field", dtype=int, default=8)
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)
79class ArgumentParserTestCase(unittest.TestCase):
80 """A test case for ArgumentParser."""
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)
91 def tearDown(self):
92 del self.ap
93 del self.config
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)
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
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)
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
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
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 )
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")
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")
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 )
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)
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 )
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])
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 )
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]
249 if shouldMatch:
250 self.assertIn("config.multiDocItem", res)
251 else:
252 self.assertNotIn("config.multiDocItem", res)
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")
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")
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 )
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")
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"))
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))
320 with self.assertRaises(SystemExit):
321 self.ap.parse_args(config=self.config,
322 args=[DataPath, "--loglevel", "1234"],
323 )
325 with self.assertRaises(SystemExit):
326 self.ap.parse_args(config=self.config,
327 args=[DataPath, "--loglevel", "INVALID_LEVEL"],
328 )
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)
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))
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)
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)
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)
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)
393 del namespace
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)
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)
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)
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 )
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)
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)
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)
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 )
479 def testOutputs(self):
480 """Test output directories, specified in different ways"""
481 parser = pipeBase.ArgumentParser(name="argumentParser")
482 self.assertTrue(parser.requireOutput)
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)
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"])
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"])
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)
519 finally:
520 shutil.rmtree(repositoryPath)
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, ])
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, [])
545class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
546 pass
549def setup_module(module):
550 lsst.utils.tests.init()
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()