# Licensed under a 3-clause BSD style license - see LICENSE.rst"""Utilities for testing."""importosimportsysfromnumpy.testingimportassert_allcloseimportastropyimportastropy.unitsasufromastropy.coordinatesimportSkyCoordfromastropy.timeimportTimefromastropy.utils.introspectionimportminversionimportmatplotlib.pyplotasplt__all__=["assert_quantity_allclose","assert_skycoord_allclose","assert_time_allclose","Checker","mpl_plot_check","requires_data","requires_dependency",]ASTROPY_LT_5_3=minversion(astropy,"5.3.dev")# Cache for `requires_dependency`_requires_dependency_cache={}
[docs]defrequires_dependency(name):"""Decorator to declare required dependencies for tests. Examples -------- :: from gammapy.utils.testing import requires_dependency @requires_dependency('scipy') def test_using_scipy(): import scipy ... """importpytestifnamein_requires_dependency_cache:skip_it=_requires_dependency_cache[name]else:try:__import__(name)skip_it=FalseexceptImportError:skip_it=True_requires_dependency_cache[name]=skip_itreason=f"Missing dependency: {name}"returnpytest.mark.skipif(skip_it,reason=reason)
defhas_data(name):"""Check if certain set of data available."""ifname=="gammapy-extra":return"GAMMAPY_EXTRA"inos.environelifname=="gammapy-data":return"GAMMAPY_DATA"inos.environelifname=="gamma-cat":return"GAMMA_CAT"inos.environelifname=="fermi-lat":return"GAMMAPY_FERMI_LAT_DATA"inos.environelse:raiseValueError(f"Invalid name: {name}")
[docs]defrequires_data(name="gammapy-data"):"""Decorator to declare required data for tests. Examples -------- :: from gammapy.utils.testing import requires_data @requires_data() def test_using_data_files(): filename = "$GAMMAPY_DATA/..." ... """importpytestifnotisinstance(name,str):raiseTypeError("You must call @requires_data with a name (str). ""Usually this: @requires_data()")skip_it=nothas_data(name)reason=f"Missing data: {name}"returnpytest.mark.skipif(skip_it,reason=reason)
defrun_cli(cli,args,exit_code=0):"""Run Click command line tool. Thin wrapper around `click.testing.CliRunner` that prints info to stderr if the command fails. Parameters ---------- cli : click.Command Click command. args : list of str Argument list. exit_code : int, optional Expected exit code of the command. Default is 0. Returns ------- result : `click.testing.Result` Result. """fromclick.testingimportCliRunnerresult=CliRunner().invoke(cli,args,catch_exceptions=False)ifresult.exit_code!=exit_code:sys.stderr.write("Exit code mismatch!\n")sys.stderr.write("Output:\n")sys.stderr.write(result.output)returnresult
[docs]defassert_skycoord_allclose(actual,desired):"""Assert all-close for `astropy.coordinates.SkyCoord` objects. - Frames can be different, aren't checked at the moment. """assertisinstance(actual,SkyCoord)assertisinstance(desired,SkyCoord)assert_allclose(actual.data.lon.deg,desired.data.lon.deg)assert_allclose(actual.data.lat.deg,desired.data.lat.deg)
[docs]defassert_time_allclose(actual,desired,atol=1e-3):"""Assert all-close for `astropy.time.Time` objects. atol : Absolute tolerance in seconds. Default is 1e-3. """assertisinstance(actual,Time)assertisinstance(desired,Time)assertactual.scale==desired.scaleassertactual.format==desired.formatdt=actual-desiredassert_allclose(dt.sec,0,rtol=0,atol=atol)
[docs]defassert_quantity_allclose(actual,desired,rtol=1.0e-7,atol=None,**kwargs):"""Assert all-close for `~astropy.units.Quantity` objects. Notes _____ Requires that ``unit`` is identical, not just that quantities are allclose taking different units into account. We prefer this kind of assert for testing, since units should only change on purpose, so this tests more behaviour. """# TODO: change this later to explicitly check units are the same!# assert actual.unit == desired.unitargs=_unquantify_allclose_arguments(actual,desired,rtol,atol)assert_allclose(*args,**kwargs)
def_unquantify_allclose_arguments(actual,desired,rtol,atol):actual=u.Quantity(actual,subok=True,copy=False)desired=u.Quantity(desired,subok=True,copy=False)try:desired=desired.to(actual.unit)exceptu.UnitsError:raiseu.UnitsError("Units for 'desired' ({}) and 'actual' ({}) ""are not convertible".format(desired.unit,actual.unit))ifatolisNone:# by default, we assume an absolute tolerance of 0atol=u.Quantity(0)else:atol=u.Quantity(atol,subok=True,copy=False)try:atol=atol.to(actual.unit)exceptu.UnitsError:raiseu.UnitsError("Units for 'atol' ({}) and 'actual' ({}) ""are not convertible".format(atol.unit,actual.unit))rtol=u.Quantity(rtol,subok=True,copy=False)try:rtol=rtol.to(u.dimensionless_unscaled)exceptException:raiseu.UnitsError("`rtol` should be dimensionless")returnactual.value,desired.value,rtol.value,atol.value
[docs]defmpl_plot_check():"""Matplotlib plotting test context manager. Create a new figure on __enter__ and calls savefig for the current figure in __exit__. This will trigger a render of the Figure, which can sometimes raise errors if there is a problem. This is writing to an in-memory byte buffer, i.e. is faster than writing to disk. """fromioimportBytesIOclassMPLPlotCheck:def__enter__(self):plt.figure()def__exit__(self,type,value,traceback):plt.savefig(BytesIO(),format="png")plt.close()returnMPLPlotCheck()
[docs]classChecker:"""Base class for checker classes in Gammapy."""
UNIT_REPLACEMENTS_ASTROPY_5_3={"cm2 s TeV":"TeV s cm2","1 / (cm2 s)":"1 / (s cm2)","erg / (cm2 s)":"erg / (s cm2)",}defmodify_unit_order_astropy_5_3(expected_str):"""Modify unit order for tests with astropy >= 5.3."""ifASTROPY_LT_5_3:forold,newinUNIT_REPLACEMENTS_ASTROPY_5_3.items():expected_str=expected_str.replace(old,new)returnexpected_str