LSSTApplications  20.0.0
LSSTDataManagementBasePackage
ingest_tests.py
Go to the documentation of this file.
1 # This file is part of obs_base.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 """Base class for writing Gen3 raw data ingest tests.
23 """
24 
25 __all__ = ("IngestTestBase",)
26 
27 import abc
28 import click.testing
29 import tempfile
30 import unittest
31 import os
32 import shutil
33 
34 from lsst.daf.butler import Butler
35 from lsst.daf.butler.cli.butler import cli as butlerCli
36 import lsst.obs.base
37 from lsst.utils import doImport
38 from .utils import getInstrument
39 
40 
41 class IngestTestBase(metaclass=abc.ABCMeta):
42  """Base class for tests of gen3 ingest. Subclass from this, then
43  `unittest.TestCase` to get a working test suite.
44  """
45 
46  ingestDir = ""
47  """Root path to ingest files into. Typically `obs_package/tests/`; the
48  actual directory will be a tempdir under this one.
49  """
50 
51  dataIds = []
52  """list of butler data IDs of files that should have been ingested."""
53 
54  file = ""
55  """Full path to a file to ingest in tests."""
56 
57  rawIngestTask = "lsst.obs.base.RawIngestTask"
58  """The task to use in the Ingest test."""
59 
60  curatedCalibrationDatasetTypes = None
61  """List or tuple of Datasets types that should be present after calling
62  writeCuratedCalibrations. If `None` writeCuratedCalibrations will
63  not be called and the test will be skipped."""
64 
65  defineVisitsTask = lsst.obs.base.DefineVisitsTask
66  """The task to use to define visits from groups of exposures.
67  This is ignored if ``visits`` is `None`.
68  """
69 
70  visits = {}
71  """A dictionary mapping visit data IDs the lists of exposure data IDs that
72  are associated with them.
73  If this is empty (but not `None`), visit definition will be run but no
74  visits will be expected (e.g. because no exposures are on-sky
75  observations).
76  """
77 
78  outputRun = "raw"
79  """The name of the output run to use in tests.
80  """
81 
82  @property
83  @abc.abstractmethod
85  """The fully qualified instrument class name.
86 
87  Returns
88  -------
89  `str`
90  The fully qualified instrument class name.
91  """
92  pass
93 
94  @property
95  def instrumentClass(self):
96  """The instrument class."""
97  return doImport(self.instrumentClassName)
98 
99  @property
100  def instrumentName(self):
101  """The name of the instrument.
102 
103  Returns
104  -------
105  `str`
106  The name of the instrument.
107  """
108  return self.instrumentClass.getName()
109 
110  def setUp(self):
111  # Use a temporary working directory
112  self.root = tempfile.mkdtemp(dir=self.ingestDir)
113  self._createRepo()
114 
115  # Register the instrument and its static metadata
116  self._registerInstrument()
117 
118  def tearDown(self):
119  if os.path.exists(self.root):
120  shutil.rmtree(self.root, ignore_errors=True)
121 
122  def verifyIngest(self, files=None, cli=False):
123  """
124  Test that RawIngestTask ingested the expected files.
125 
126  Parameters
127  ----------
128  files : `list` [`str`], or None
129  List of files to be ingested, or None to use ``self.file``
130  """
131  butler = Butler(self.root, run=self.outputRun)
132  datasets = butler.registry.queryDatasets(self.outputRun, collections=...)
133  self.assertEqual(len(list(datasets)), len(self.dataIds))
134  for dataId in self.dataIds:
135  exposure = butler.get(self.outputRun, dataId)
136  metadata = butler.get("raw.metadata", dataId)
137  self.assertEqual(metadata.toDict(), exposure.getMetadata().toDict())
138 
139  # Since components follow a different code path we check that
140  # WCS match and also we check that at least the shape
141  # of the image is the same (rather than doing per-pixel equality)
142  wcs = butler.get("raw.wcs", dataId)
143  self.assertEqual(wcs, exposure.getWcs())
144 
145  rawImage = butler.get("raw.image", dataId)
146  self.assertEqual(rawImage.getBBox(), exposure.getBBox())
147 
148  self.checkRepo(files=files)
149 
150  def checkRepo(self, files=None):
151  """Check the state of the repository after ingest.
152 
153  This is an optional hook provided for subclasses; by default it does
154  nothing.
155 
156  Parameters
157  ----------
158  files : `list` [`str`], or None
159  List of files to be ingested, or None to use ``self.file``
160  """
161  pass
162 
163  def _createRepo(self):
164  """Use the Click `testing` module to call the butler command line api
165  to create a repository."""
166  runner = click.testing.CliRunner()
167  result = runner.invoke(butlerCli, ["create", self.root])
168  self.assertEqual(result.exit_code, 0, f"output: {result.output} exception: {result.exception}")
169 
170  def _ingestRaws(self, transfer):
171  """Use the Click `testing` module to call the butler command line api
172  to ingest raws.
173 
174  Parameters
175  ----------
176  transfer : `str`
177  The external data transfer type.
178  """
179  runner = click.testing.CliRunner()
180  result = runner.invoke(butlerCli, ["ingest-raws", self.root,
181  "--output-run", self.outputRun,
182  "--file", self.file,
183  "--transfer", transfer,
184  "--ingest-task", self.rawIngestTask])
185  self.assertEqual(result.exit_code, 0, f"output: {result.output} exception: {result.exception}")
186 
187  def _registerInstrument(self):
188  """Use the Click `testing` module to call the butler command line api
189  to register the instrument."""
190  runner = click.testing.CliRunner()
191  result = runner.invoke(butlerCli, ["register-instrument", self.root,
192  "--instrument", self.instrumentClassName])
193  self.assertEqual(result.exit_code, 0, f"output: {result.output} exception: {result.exception}")
194 
195  def _writeCuratedCalibrations(self):
196  """Use the Click `testing` module to call the butler command line api
197  to write curated calibrations."""
198  runner = click.testing.CliRunner()
199  result = runner.invoke(butlerCli, ["write-curated-calibrations", self.root,
200  "--instrument", self.instrumentName,
201  "--output-run", self.outputRun])
202  self.assertEqual(result.exit_code, 0, f"output: {result.output} exception: {result.exception}")
203 
204  def testLink(self):
205  self._ingestRaws(transfer="link")
206  self.verifyIngest()
207 
208  def testSymLink(self):
209  self._ingestRaws(transfer="symlink")
210  self.verifyIngest()
211 
212  def testCopy(self):
213  self._ingestRaws(transfer="copy")
214  self.verifyIngest()
215 
216  def testHardLink(self):
217  try:
218  self._ingestRaws(transfer="hardlink")
219  self.verifyIngest()
220  except PermissionError as err:
221  raise unittest.SkipTest("Skipping hard-link test because input data"
222  " is on a different filesystem.") from err
223 
224  def testInPlace(self):
225  """Test that files already in the directory can be added to the
226  registry in-place.
227  """
228  # symlink into repo root manually
229  butler = Butler(self.root, run=self.outputRun)
230  newPath = os.path.join(butler.datastore.root, os.path.basename(self.file))
231  os.symlink(os.path.abspath(self.file), newPath)
232  self._ingestRaws(transfer=None)
233  self.verifyIngest()
234 
236  """Re-ingesting the same data into the repository should fail.
237  """
238  self._ingestRaws(transfer="symlink")
239  with self.assertRaises(Exception):
240  self._ingestRaws(transfer="symlink")
241 
243  """Test that we can ingest the curated calibrations"""
244  if self.curatedCalibrationDatasetTypes is None:
245  raise unittest.SkipTest("Class requests disabling of writeCuratedCalibrations test")
246 
248 
249  dataId = {"instrument": self.instrumentName}
250  butler = Butler(self.root, run=self.outputRun)
251  for datasetTypeName in self.curatedCalibrationDatasetTypes:
252  with self.subTest(dtype=datasetTypeName, dataId=dataId):
253  datasets = list(butler.registry.queryDatasets(datasetTypeName, collections=...,
254  dataId=dataId))
255  self.assertGreater(len(datasets), 0, f"Checking {datasetTypeName}")
256 
257  def testDefineVisits(self):
258  if self.visits is None:
259  self.skipTest("Expected visits were not defined.")
260  self._ingestRaws(transfer="link")
261 
262  config = self.defineVisitsTask.ConfigClass()
263  butler = Butler(self.root, run=self.outputRun)
264  instr = getInstrument(self.instrumentName, butler.registry)
265  instr.applyConfigOverrides(self.defineVisitsTask._DefaultName, config)
266  task = self.defineVisitsTask(config=config, butler=butler)
267  task.run(self.dataIds)
268 
269  # Test that we got the visits we expected.
270  visits = set(butler.registry.queryDimensions(["visit"], expand=True))
271  self.assertCountEqual(visits, self.visits.keys())
272  camera = instr.getCamera()
273  for foundVisit, (expectedVisit, expectedExposures) in zip(visits, self.visits.items()):
274  # Test that this visit is associated with the expected exposures.
275  foundExposures = set(butler.registry.queryDimensions(["exposure"], dataId=expectedVisit,
276  expand=True))
277  self.assertCountEqual(foundExposures, expectedExposures)
278  # Test that we have a visit region, and that it contains all of the
279  # detector+visit regions.
280  self.assertIsNotNone(foundVisit.region)
281  detectorVisitDataIds = set(butler.registry.queryDimensions(["visit", "detector"],
282  dataId=expectedVisit,
283  expand=True))
284  self.assertEqual(len(detectorVisitDataIds), len(camera))
285  for dataId in detectorVisitDataIds:
286  self.assertTrue(foundVisit.region.contains(dataId.region))
lsst.obs.base.ingest_tests.IngestTestBase.verifyIngest
def verifyIngest(self, files=None, cli=False)
Definition: ingest_tests.py:122
lsst.obs.base.ingest_tests.IngestTestBase.setUp
def setUp(self)
Definition: ingest_tests.py:110
lsst.obs.base.utils.getInstrument
def getInstrument(instrumentName, registry=None)
Definition: utils.py:100
lsst.obs.base.ingest_tests.IngestTestBase.testHardLink
def testHardLink(self)
Definition: ingest_tests.py:216
lsst::meas::modelfit.psf.psfContinued.ConfigClass
ConfigClass
Definition: psfContinued.py:56
astshim.keyMap.keyMapContinued.keys
def keys(self)
Definition: keyMapContinued.py:6
lsst.obs.base.ingest_tests.IngestTestBase.testInPlace
def testInPlace(self)
Definition: ingest_tests.py:224
lsst.obs.base.ingest_tests.IngestTestBase.testCopy
def testCopy(self)
Definition: ingest_tests.py:212
lsst.obs.base.ingest_tests.IngestTestBase.visits
dictionary visits
Definition: ingest_tests.py:70
lsst.obs.base.ingest_tests.IngestTestBase.ingestDir
string ingestDir
Definition: ingest_tests.py:46
lsst.obs.base.ingest_tests.IngestTestBase.instrumentName
def instrumentName(self)
Definition: ingest_tests.py:100
lsst.obs.base.ingest_tests.IngestTestBase.dataIds
list dataIds
Definition: ingest_tests.py:51
lsst.obs.base.ingest_tests.IngestTestBase.tearDown
def tearDown(self)
Definition: ingest_tests.py:118
lsst.obs.base.ingest_tests.IngestTestBase.defineVisitsTask
defineVisitsTask
Definition: ingest_tests.py:65
lsst.obs.base.ingest_tests.IngestTestBase.testSymLink
def testSymLink(self)
Definition: ingest_tests.py:208
lsst::daf::persistence.utils.doImport
def doImport(pythonType)
Definition: utils.py:104
lsst.obs.base.ingest_tests.IngestTestBase._ingestRaws
def _ingestRaws(self, transfer)
Definition: ingest_tests.py:170
lsst.obs.base.ingest_tests.IngestTestBase.testWriteCuratedCalibrations
def testWriteCuratedCalibrations(self)
Definition: ingest_tests.py:242
lsst.obs.base.ingest_tests.IngestTestBase._registerInstrument
def _registerInstrument(self)
Definition: ingest_tests.py:187
lsst::utils
Definition: Backtrace.h:29
lsst.obs.base.defineVisits.DefineVisitsTask
Definition: defineVisits.py:250
lsst.obs.base.ingest_tests.IngestTestBase.instrumentClass
def instrumentClass(self)
Definition: ingest_tests.py:95
lsst.obs.base.ingest_tests.IngestTestBase._createRepo
def _createRepo(self)
Definition: ingest_tests.py:163
items
std::vector< SchemaItem< Flag > > * items
Definition: BaseColumnView.cc:142
list
daf::base::PropertyList * list
Definition: fits.cc:913
lsst.obs.base.ingest_tests.IngestTestBase.curatedCalibrationDatasetTypes
curatedCalibrationDatasetTypes
Definition: ingest_tests.py:60
lsst.obs.base.ingest_tests.IngestTestBase.root
root
Definition: ingest_tests.py:112
lsst.obs.base.ingest_tests.IngestTestBase.testLink
def testLink(self)
Definition: ingest_tests.py:204
lsst.obs.base.ingest_tests.IngestTestBase.file
string file
Definition: ingest_tests.py:54
lsst.obs.base.ingest_tests.IngestTestBase.testDefineVisits
def testDefineVisits(self)
Definition: ingest_tests.py:257
lsst.obs.base.ingest_tests.IngestTestBase._writeCuratedCalibrations
def _writeCuratedCalibrations(self)
Definition: ingest_tests.py:195
lsst.obs.base.ingest_tests.IngestTestBase.outputRun
string outputRun
Definition: ingest_tests.py:78
lsst.obs.base.ingest_tests.IngestTestBase.checkRepo
def checkRepo(self, files=None)
Definition: ingest_tests.py:150
set
daf::base::PropertySet * set
Definition: fits.cc:912
lsst.obs.base.ingest_tests.IngestTestBase.testFailOnConflict
def testFailOnConflict(self)
Definition: ingest_tests.py:235
lsst.obs.base.ingest_tests.IngestTestBase
Definition: ingest_tests.py:41
lsst.obs.base.ingest_tests.IngestTestBase.instrumentClassName
def instrumentClassName(self)
Definition: ingest_tests.py:84
lsst.obs.base
Definition: __init__.py:1