def test_sextractor(self): # Note that, being a third-party program, we cannot write a unit test # to check that SExtractor works as it should (i.e., that it correctly # detects astronomical sources on FITS images): that is something that # Emmanuel Bertin, its developer, certainly takes care of. What we must # test for is whether astromatic.sextractor(), our Python wrapper to # call SExtractor, works as we expect and raises the appropriate errors # when something goes wrong. Nothing less, nothing more. # First, the most probable scenario: our wrapper should run SExtractor # on any FITS image that it receives (here we use some downloaded from # the STScI Digitized Sky Survey) and return the path to the output # catalog, which astromatic.Catalog should be always able to parse. kwargs = dict(stdout=open(os.devnull), stderr=open(os.devnull)) for img_path in dss_images.TEST_IMAGES: catalog_path = astromatic.sextractor(img_path, **kwargs) try: Catalog(catalog_path) finally: os.unlink(catalog_path) # SExtractorNotInstalled must be raised if no SExtractor executable is # detected. In order to simulate this, mock os.environ and clear the # 'PATH' environment variable. In this manner, as the list of paths to # directories where executables may be found is empty, SExtractor (and # any other command) will appear as not installed on the system. environment_copy = copy.deepcopy(os.environ) with mock.patch.object(os, 'environ', environment_copy) as mocked: mocked['PATH'] = '' with self.assertRaises(astromatic.SExtractorNotInstalled): astromatic.sextractor(img_path) # IOError is raised if any of the four SExtractor configuration files # is not readable or does no exist. To test that, mock the module-level # variables so that they first refer to an unreadable file and later to # a nonexistent one. for variable in self.SEXTRACTOR_MODULE_VARS: path = eval('astromatic.%s' % variable) ext = os.path.splitext(path)[1] copy_path = get_nonexistent_path(ext=ext) shutil.copy2(path, copy_path) with mock.patch.object(astromatic, variable, copy_path): # chmod u-r mode = stat.S_IMODE(os.stat(copy_path)[stat.ST_MODE]) mode ^= stat.S_IRUSR os.chmod(copy_path, mode) args = IOError, astromatic.sextractor, img_path self.assertFalse(os.access(copy_path, os.R_OK)) self.assertRaises(*args) os.unlink(copy_path) self.assertFalse(os.path.exists(copy_path)) self.assertRaises(*args) # SExtractorUpgradeRequired must be raised if the version of SExtractor # that is installed on the system is older than that defined in the # SEXTRACTOR_REQUIRED_VERSION module-level variable. We cannot (easily, # at least) change the version that is installed, but we can test for # this by changing the value of the required version, setting it to a # tuple that is greater than the installed version of SExtractor. # From, for example, (2, 8, 6) to (2, 8, 7) version = list(astromatic.sextractor_version()) version[-1] += 1 version = tuple(version) with mock.patch.object(astromatic, 'SEXTRACTOR_REQUIRED_VERSION', version): with self.assertRaises(astromatic.SExtractorUpgradeRequired): astromatic.sextractor(img_path) # The SExtractorError exception is raised if anything goes wrong during # the execution of SExtractor (in more technical terms, if its return # code is other than zero). For example, the FITS image may not exist, # or a non-numerical value may be assigned to a parameter that expects # one, or we could try to run SExtractor on a FITS extension that does # not exist. These three are just examples: SExtractor may fail for # many different reasons that we cannot even foresee. # (1) Try to run SExtractor on a non-existent image kwargs = dict(stdout=open(os.devnull), stderr=open(os.devnull)) nonexistent_path = get_nonexistent_path(ext='.fits') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(nonexistent_path, **kwargs) # (2) The DETECT_MINAREA parameter (minimum number of pixels above the # threshold needed to trigger detection) expects an integer. Assigning # to it a string will cause SExtractor to complain ("keyword out of # range") and abort its execution. kwargs['options'] = dict(DETECT_MINAREA='Y') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) del kwargs['options'] # (3) Try to run SExtractor on a nonexistent FITS extension. hdulist = pyfits.open(img_path, mode='readonly') nextensions = len(hdulist) hdulist.close() kwargs['ext'] = nextensions + 1 with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) # TypeError raised if 'options' is not a dictionary kwargs = dict(options=['DETECT_MINAREA', '5']) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... or if any of its elements is not a string. kwargs['options'] = {'DETECT_MINAREA': 125} with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # TypeError also raised if 'ext' is not an integer... kwargs = dict(ext=0.56) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... even if it is a float but has nothing after the decimal point # (for which the built-in is_integer() function would return True). kwargs = dict(ext=0.0) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs)
def test_sextractor(self): # Note that, being a third-party program, we cannot write a unit test # to check that SExtractor works as it should (i.e., that it correctly # detects astronomical sources on FITS images): that is something that # Emmanuel Bertin, its developer, certainly takes care of. What we must # test for is whether astromatic.sextractor(), our Python wrapper to # call SExtractor, works as we expect and raises the appropriate errors # when something goes wrong. Nothing less, nothing more. # First, the most probable scenario: our wrapper should run SExtractor # on any FITS image that it receives (here we use some downloaded from # the STScI Digitized Sky Survey) and return the path to the output # catalog, which astromatic.Catalog should be always able to parse. kwargs = dict(stdout = open(os.devnull), stderr = open(os.devnull)) for img_path in dss_images.TEST_IMAGES: catalog_path = astromatic.sextractor(img_path, **kwargs) try: Catalog(catalog_path) finally: os.unlink(catalog_path) # SExtractorNotInstalled must be raised if no SExtractor executable is # detected. In order to simulate this, mock os.environ and clear the # 'PATH' environment variable. In this manner, as the list of paths to # directories where executables may be found is empty, SExtractor (and # any other command) will appear as not installed on the system. environment_copy = copy.deepcopy(os.environ) with mock.patch.object(os, 'environ', environment_copy) as mocked: mocked['PATH'] = '' with self.assertRaises(astromatic.SExtractorNotInstalled): astromatic.sextractor(img_path) # IOError is raised if any of the four SExtractor configuration files # is not readable or does no exist. To test that, mock the module-level # variables so that they first refer to an unreadable file and later to # a nonexistent one. for variable in self.SEXTRACTOR_MODULE_VARS: path = eval('astromatic.%s' % variable) ext = os.path.splitext(path)[1] copy_path = get_nonexistent_path(ext = ext) shutil.copy2(path, copy_path) with mock.patch.object(astromatic, variable, copy_path): # chmod u-r mode = stat.S_IMODE(os.stat(copy_path)[stat.ST_MODE]) mode ^= stat.S_IRUSR os.chmod(copy_path, mode) args = IOError, astromatic.sextractor, img_path self.assertFalse(os.access(copy_path, os.R_OK)) self.assertRaises(*args) os.unlink(copy_path) self.assertFalse(os.path.exists(copy_path)) self.assertRaises(*args) # SExtractorUpgradeRequired must be raised if the version of SExtractor # that is installed on the system is older than that defined in the # SEXTRACTOR_REQUIRED_VERSION module-level variable. We cannot (easily, # at least) change the version that is installed, but we can test for # this by changing the value of the required version, setting it to a # tuple that is greater than the installed version of SExtractor. # From, for example, (2, 8, 6) to (2, 8, 7) version = list(astromatic.sextractor_version()) version[-1] += 1 version = tuple(version) with mock.patch.object(astromatic, 'SEXTRACTOR_REQUIRED_VERSION', version): with self.assertRaises(astromatic.SExtractorUpgradeRequired): astromatic.sextractor(img_path) # The SExtractorError exception is raised if anything goes wrong during # the execution of SExtractor (in more technical terms, if its return # code is other than zero). For example, the FITS image may not exist, # or a non-numerical value may be assigned to a parameter that expects # one, or we could try to run SExtractor on a FITS extension that does # not exist. These three are just examples: SExtractor may fail for # many different reasons that we cannot even foresee. # (1) Try to run SExtractor on a non-existent image kwargs = dict(stdout = open(os.devnull), stderr = open(os.devnull)) nonexistent_path = get_nonexistent_path(ext = '.fits') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(nonexistent_path, **kwargs) # (2) The DETECT_MINAREA parameter (minimum number of pixels above the # threshold needed to trigger detection) expects an integer. Assigning # to it a string will cause SExtractor to complain ("keyword out of # range") and abort its execution. kwargs['options'] = dict(DETECT_MINAREA = 'Y') with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) del kwargs['options'] # (3) Try to run SExtractor on a nonexistent FITS extension. hdulist = pyfits.open(img_path, mode = 'readonly') nextensions = len(hdulist) hdulist.close() kwargs['ext'] = nextensions + 1 with self.assertRaises(astromatic.SExtractorError): astromatic.sextractor(img_path, **kwargs) # TypeError raised if 'options' is not a dictionary kwargs = dict(options = ['DETECT_MINAREA', '5']) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... or if any of its elements is not a string. kwargs['options'] = {'DETECT_MINAREA' : 125} with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # TypeError also raised if 'ext' is not an integer... kwargs = dict(ext = 0.56) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs) # ... even if it is a float but has nothing after the decimal point # (for which the built-in is_integer() function would return True). kwargs = dict(ext = 0.0) with self.assertRaises(TypeError): astromatic.sextractor(img_path, **kwargs)
# This point only reached if on-disk catalog can be reused msg = "%s: on-disk catalog exists and MD5 hashes match. Yay!" logging.debug(msg % self.path) except (KeyError, IOError, ValueError): msg = ("%s: could not reuse an existing, on-disk cached catalog; " "SExtractor must be run") % self.path logging.debug(msg) # Redirect standard and error outputs to null device with open(os.devnull, 'wt') as fd: logging.info("%s: running SExtractor" % self.path) self.catalog_path = \ astromatic.sextractor(self.path, options = options, stdout = fd, stderr = fd) logging.debug("%s: SExtractor OK" % self.path) try: # Update the FITS header with the path and MD5; give up # silently in case we do not have permissions to do it # (IOError) or if the length of the HIERARCH keyword, equal # sign and value is longer than 80 characters (ValueError, # see FITSImage.update_keyword() for details). The cast to # str is needed because PyFITS has complained sometimes # about "illegal values" if it receives a Unicode string. self.update_keyword(keywords.sex_catalog, str(self.catalog_path)) self.update_keyword(keywords.sex_md5sum, sex_md5sum) except (IOError, ValueError): pass
# This point only reached if on-disk catalog can be reused msg = "%s: on-disk catalog exists and MD5 hashes match. Yay!" logging.debug(msg % self.path) except (KeyError, IOError, ValueError): msg = ("%s: could not reuse an existing, on-disk cached catalog; " "SExtractor must be run") % self.path logging.debug(msg) # Redirect standard and error outputs to null device with open(os.devnull, 'wt') as fd: logging.info("%s: running SExtractor" % self.path) self.catalog_path = \ astromatic.sextractor(self.path, options = options, stdout = fd, stderr = fd) logging.debug("%s: SExtractor OK" % self.path) try: # Update the FITS header with the path and MD5; give up # silently in case we do not have permissions to do it # (IOError) or if the length of the HIERARCH keyword, equal # sign and value is longer than 80 characters (ValueError, # see FITSImage.update_keyword() for details). The cast to # str is needed because PyFITS has complained sometimes # about "illegal values" if it receives a Unicode string. self.update_keyword(keywords.sex_catalog, str(self.catalog_path)) self.update_keyword(keywords.sex_md5sum, sex_md5sum) except (IOError, ValueError):