Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "tests/data/snapred-data"]
path = tests/data/snapred-data
url = https://code.ornl.gov/sns-hfir-scse/infrastructure/test-data/snapred-data.git
branch = main
2 changes: 2 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ channels:
dependencies:
- snapred==1.2.0rc2
# SNAPBlue specific dependencies
- snapred==1.1.0rc5
- scikit-image
- git-lfs
# -- Runtime dependencies
# base: list all base dependencies here
- python>=3.8 # please specify the minimum version of python here
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,18 @@ packagename-cli = "packagenamepy.packagename:main"
packagenamepy = "packagenamepy.packagename:gui"

[tool.pytest.ini_options]
addopts = "-v --cov=packagenamepy --cov-report=term-missing"
addopts = "-m 'not (integration or datarepo)' -v --cov=packagenamepy --cov-report=term-missing"
pythonpath = [
".", "src", "scripts"
]
testpaths = ["tests"]
python_files = ["test*.py"]
norecursedirs = [".git", "tmp*", "_tmp*", "__pycache__", "*dataset*", "*data_set*"]
markers = [
"mymarker: example markers goes here"
"integration: mark a test as an integration test",
"mount_snap: mark a test as using /SNS/SNAP/ data mount",
"golden_data(*, path=None, short_name=None, date=None): mark golden data to use with a test",
"datarepo: mark a test as using snapred-data repo"
]

[tool.ruff]
Expand Down
86 changes: 86 additions & 0 deletions src/snapblue/meta/Config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import importlib.resources as resources

import os
import sys

from pathlib import Path

from snapred.meta.Config import Resource as RedResource, Config

def _find_root_dir():
try:
MODULE_ROOT = Path(sys.modules["snapblue"].__file__).parent

# Using `"test" in env` here allows different versions of "[category]_test.yml" to be used for different
# test categories: e.g. unit tests use "test.yml" but integration tests use "integration_test.yml".
env = os.environ.get("snapblue_env")
if env and "test" in env and "conftest" in sys.modules:
# WARNING: there are now multiple "conftest.py" at various levels in the test hierarchy.
MODULE_ROOT = MODULE_ROOT.parent.parent / "tests"
except Exception as e:
raise RuntimeError("Unable to determine SNAPBlue module-root directory") from e

return str(MODULE_ROOT)

class _Resource:
_packageMode: bool
_resourcesPath: str

def __init__(self):
# where the location of resources are depends on whether or not this is in package mode
self._packageMode = not self._existsInPackage("application.yml")
if self._packageMode:
self._resourcesPath = "/resources/"
else:
self._resourcesPath = os.path.join(_find_root_dir(), "resources/")

def _existsInPackage(self, subPath) -> bool:
with resources.path("snapblue.resources", subPath) as path:
return os.path.exists(path)

def exists(self, subPath) -> bool:
if self._packageMode:
return self._existsInPackage(subPath)
else:
return os.path.exists(self.getPath(subPath))

def getPath(self, subPath):
if subPath.startswith("/"):
return os.path.join(self._resourcesPath, subPath[1:])
else:
return os.path.join(self._resourcesPath, subPath)

def read(self, subPath):
with self.open(subPath, "r") as file:
return file.read()

def open(self, subPath, mode): # noqa: A003
if self._packageMode:
with resources.path("snapblue.resources", subPath) as path:
return open(path, mode)
else:
return open(self.getPath(subPath), mode)


Resource = _Resource()
RedResource._resourcesPath = Resource._resourcesPath
RedResource._packageMode = Resource._packageMode
# use refresh to do initial load, clearing shouldn't matter
Config.refresh("application.yml")

# ---------- SNAPRed-internal values: --------------------------
# allow "resources" relative paths to be entered into the "yml"
# using "${module.root}"
Config._config["module"] = {}
Config._config["module"]["root"] = _find_root_dir()

Config._config["version"] = Config._config.get("version", {})
Config._config["version"]["default"] = -1
# ---------- end: internal values: -----------------------------

# see if user used environment injection to modify what is needed
# this will get from the os environment or from the currently loaded one
# first case wins
env = os.environ.get("snapblue_env", Config._config.get("environment", None))
if env is not None:
Config.refresh(env)
4 changes: 4 additions & 0 deletions src/snapblue/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# put snapred application.yml overrides here
IPTS:
default: /SNS
root: /SNS
143 changes: 143 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import os

import pytest
import unittest.mock as mock

from snapred.meta.decorators import Resettable
from snapred.meta.decorators.Singleton import reset_Singletons

# import sys
# sys.path.append('.')
# os.environ['PYTHONPATH'] = './src'

# Allow override: e.g. `env=dev pytest ...`
# if not os.environ.get("env"):
# os.environ["env"] = "test"

from mantid.kernel import ConfigService # noqa: E402
from snapblue.meta.Config import ( # noqa: E402
Config, # noqa: E402
Resource, # noqa: E402
)


# PATCH the `unittest.mock.Mock` class: BANNED FUNCTIONS
def banned_function(function_name: str):
_error_message: str = f"`Mock.{function_name}` is a mock, it always evaluates to True. Use `Mock.assert_{function_name}` instead."

def _banned_function(self, *args, **kwargs):
nonlocal _error_message # this line should not be necessary!

# Ensure that the complete message is in the pytest-captured output stream:
print(_error_message)

raise RuntimeError(_error_message)

return _banned_function

# `mock.Mock.called` is OK: it exists as a boolean attribute
mock.Mock.called_once = banned_function("called_once")
mock.Mock.called_once_with = banned_function("called_once_with")
mock.Mock.called_with = banned_function("called_with")
mock.Mock.not_called = banned_function("not_called")


def mock_decorator(orig_cls):
return orig_cls

###### PATCH THE DECORATORS HERE ######

mockResettable = mock.Mock()
mockResettable.Resettable = mock_decorator
mock.patch.dict("sys.modules", {"snapred.meta.decorators.Resettable": mockResettable}).start()
mock.patch.dict("sys.modules", {"snapred.meta.decorators._Resettable": Resettable}).start()

mantidConfig = config = ConfigService.Instance()
mantidConfig["CheckMantidVersion.OnStartup"] = "0"
mantidConfig["UpdateInstrumentDefinitions.OnStartup"] = "0"
mantidConfig["usagereports.enabled"] = "0"

#######################################

# this at teardown removes the loggers, eliminating logger-related error printouts
# see https://github.com/pytest-dev/pytest/issues/5502#issuecomment-647157873
@pytest.fixture(autouse=True, scope="session")
def clear_loggers(): # noqa: PT004
"""Remove handlers from all loggers"""
import logging

yield # ... teardown follows:
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
for logger in loggers:
handlers = getattr(logger, "handlers", [])
for handler in handlers:
logger.removeHandler(handler)

########################################################################################################################
# In combination, the following autouse fixtures allow unit tests and integration tests
# to be run successfully without mocking out the `@Singleton` decorator.
#
# * The main objective is to allow the `@Singleton` classes to function as singletons, for the
# duration of the single-test scope. This functionality is necessary, for example, in order for
# the `Indexer` class to function correctly during the state-initialization sequence.
#
# * There are some fine points involved with using `@Singleton` classes during testing at class and module scope.
# Such usage should probably be avoided whenever possible. It's a bit tricky to get this to work correctly
# within the test framework.
#
# * TODO: Regardless of these fixtures, at the moment the `@Singleton` decorator must be completely turned ON during
# integration tests, without any modification (e.g. or "reset"). There is something going on at "session" scope
# with specific singletons not being deleted between tests, which results in multiple singleton instances when the
# fixtures are used. This behavior does not seem to be an issue for the unit tests.
# We can track this down by turning on the garbage collector `gc`, but this work has not yet been completed.
#
# Implementation notes:
#
# * Right now, there are > 36 `@Singleton` decorated classes. Probably, there should be far fewer.
# Almost none of these classes are compute-intensive to initialize, or retain any cached data.
# These would be the normal justifications for the use of this pattern.
#
# * Applying the `@Singleton` decorator changes the behavior of the classes,
# so we don't want to mock the decorator out during testing. At present, the key class where this is important
# is the `Indexer` class, which is not itself a singleton, but which is owned and cached
# by the `LocalDataService` singleton. `Indexer` instances retain local data about indexing events
# that have occurred since their initialization.
#

@pytest.fixture(autouse=True)
def _reset_Singletons(request):
if not "integration" in request.keywords:
reset_Singletons()
yield

@pytest.fixture(scope="class", autouse=True)
def _reset_class_scope_Singletons(request):
if not "integration" in request.keywords:
reset_Singletons()
yield

@pytest.fixture(scope="module", autouse=True)
def _reset_module_scope_Singletons(request):
if not "integration" in request.keywords:
reset_Singletons()
yield

########################################################################################################################


## Import various `pytest.fixture` defined in separate `tests/util` modules:
# -------------------------------------------------------------------------
# *** IMPORTANT WARNING: these must be included _after_ the `Singleton` decorator is patched ! ***
# * Otherwise, the modules imported by these will not have the patched decorator applied to them.

# from util.golden_data import goldenData, goldenDataFilePath
# from util.state_helpers import state_root_fixture
# from util.IPTS_override import IPTS_override_fixture
from util.Config_helpers import Config_override_fixture
from util.pytest_helpers import (
calibration_home_from_mirror,
cleanup_workspace_at_exit,
cleanup_class_workspace_at_exit,
get_unique_timestamp,
reduction_home_from_mirror
)
1 change: 1 addition & 0 deletions tests/data/snapred-data
Submodule snapred-data added at 60ee8c
16 changes: 16 additions & 0 deletions tests/integration/test_filesystem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@


import pytest
from util.pytest_helpers import calibration_home_from_mirror, handleStateInit, reduction_home_from_mirror # noqa: F401
from snapblue.meta.Config import Config
from pathlib import Path


@pytest.mark.integration
@pytest.mark.datarepo
def test_calibrationHomeExists(calibration_home_from_mirror):
tmpCalibrationHomeDirectory = calibration_home_from_mirror()
calibrationHomePath = Path(Config["instrument.calibration.home"])
assert calibrationHomePath.exists()
iptsHomePath = Path(Config["IPTS.root"])
assert iptsHomePath.exists()
10 changes: 10 additions & 0 deletions tests/integration/test_reduction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


import pytest
from util.pytest_helpers import calibration_home_from_mirror, handleStateInit, reduction_home_from_mirror # noqa: F401


@pytest.mark.integration
@pytest.mark.datarepo
def test_reduction(reduction_home_from_mirror):
pass
4 changes: 4 additions & 0 deletions tests/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# put snapred application.yml overrides here
IPTS:
default: /SNS
root: /SNS
86 changes: 86 additions & 0 deletions tests/resources/integration_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# environment: integration_test
# At present:
# * this "integration_test.yml" overrides "IPTS.root", and "constants.maskedPixelThreshold";
# * "module.root" will still be defined as in "test.yml".

IPTS:
# Eventually, for SNAPRed's test framework:
# this should be a shared location on "analysis.sns.gov".
# For the moment, each developer needs to set this individually to their local path.
root: ${module.root}/data/snapred-data/SNS

constants:
# For tests with '46680' this seems to be necessary.
maskedPixelThreshold: 1.0

DetectorPeakPredictor:
fwhm: 1.17741002252 # used to convert gaussian to fwhm (2 * log_e(2))
CropFactors:
lowWavelengthCrop: 0.05
lowdSpacingCrop: 0.1
highdSpacingCrop: 0.15
RawVanadiumCorrection:
numberOfSlices: 1
numberOfAnnuli: 1

instrument:
native:
pixelResolution: 72
definition:
file: ${module.root}/resources/ultralite/CRACKLE_Definition.xml
lite:
pixelResolution: 18
definition:
file: ${module.root}/resources/ultralite/CRACKLELite_Definition.xml
map:
file: ${module.root}/resources/ultralite/CRACKLELiteDataMap.xml

PVLogs:
# Swap these when running with ultralite data
# rootGroup: "entry/DASlogs"
rootGroup: "/mantid_workspace_1/logs"

# PV-log keys relating to instrument settings:
instrumentPVKeys:
- "BL3:Chop:Gbl:WavelengthReq"
- "BL3:Chop:Skf1:WavelengthUserReq"
- "det_arc1"
- "det_arc2"
- "BL3:Det:TH:BL:Frequency"
- "BL3:Mot:OpticsPos:Pos"
- "det_lin1"
- "det_lin2"

mantid:
workspace:
nameTemplate:
delimiter: "_"
template:
run: "{unit},{group},{lite},{auxiliary},{runNumber}"
diffCal:
input: "{unit},{runNumber},raw"
table: "diffract_consts,{runNumber},{version}"
output: "{unit},{group},{runNumber},{version}"
diagnostic: "diagnostic,{group},{runNumber},{version}"
mask: "diffract_consts,mask,{runNumber},{version}"
metric: "calib_metrics,{metricName},{runNumber},{version}"
timedMetric: "calib_metrics,{metricName},{runNumber},{timestamp}"
normCal:
rawVanadium: "{unit},{group},{runNumber},raw_van_corr,{version}"
focusedRawVanadium: "{unit},{group},{runNumber},raw_van_corr,{version}"
smoothedFocusedRawVanadium: "{unit},{group},{runNumber},fitted_van_corr,{version}"

calibration:
parameters:
default:
alpha: 0.1
# alpha: 1.1
beta:
- 0.02
- 0.05
# beta:
# - 1
# - 2
fitting:
# minSignal2Noise: 0.0
minSignal2Noise: 10
Loading
Loading