|
| 1 | +import os |
| 2 | + |
| 3 | +import pytest |
| 4 | +import unittest.mock as mock |
| 5 | + |
| 6 | +from snapred.meta.decorators import Resettable |
| 7 | +from snapred.meta.decorators.Singleton import reset_Singletons |
| 8 | + |
| 9 | +# import sys |
| 10 | +# sys.path.append('.') |
| 11 | +# os.environ['PYTHONPATH'] = './src' |
| 12 | + |
| 13 | +# Allow override: e.g. `env=dev pytest ...` |
| 14 | +# if not os.environ.get("env"): |
| 15 | +# os.environ["env"] = "test" |
| 16 | + |
| 17 | +from mantid.kernel import ConfigService # noqa: E402 |
| 18 | +from snapblue.meta.Config import ( # noqa: E402 |
| 19 | + Config, # noqa: E402 |
| 20 | + Resource, # noqa: E402 |
| 21 | +) |
| 22 | + |
| 23 | + |
| 24 | +# PATCH the `unittest.mock.Mock` class: BANNED FUNCTIONS |
| 25 | +def banned_function(function_name: str): |
| 26 | + _error_message: str = f"`Mock.{function_name}` is a mock, it always evaluates to True. Use `Mock.assert_{function_name}` instead." |
| 27 | + |
| 28 | + def _banned_function(self, *args, **kwargs): |
| 29 | + nonlocal _error_message # this line should not be necessary! |
| 30 | + |
| 31 | + # Ensure that the complete message is in the pytest-captured output stream: |
| 32 | + print(_error_message) |
| 33 | + |
| 34 | + raise RuntimeError(_error_message) |
| 35 | + |
| 36 | + return _banned_function |
| 37 | + |
| 38 | +# `mock.Mock.called` is OK: it exists as a boolean attribute |
| 39 | +mock.Mock.called_once = banned_function("called_once") |
| 40 | +mock.Mock.called_once_with = banned_function("called_once_with") |
| 41 | +mock.Mock.called_with = banned_function("called_with") |
| 42 | +mock.Mock.not_called = banned_function("not_called") |
| 43 | + |
| 44 | + |
| 45 | +def mock_decorator(orig_cls): |
| 46 | + return orig_cls |
| 47 | + |
| 48 | +###### PATCH THE DECORATORS HERE ###### |
| 49 | + |
| 50 | +mockResettable = mock.Mock() |
| 51 | +mockResettable.Resettable = mock_decorator |
| 52 | +mock.patch.dict("sys.modules", {"snapred.meta.decorators.Resettable": mockResettable}).start() |
| 53 | +mock.patch.dict("sys.modules", {"snapred.meta.decorators._Resettable": Resettable}).start() |
| 54 | + |
| 55 | +mantidConfig = config = ConfigService.Instance() |
| 56 | +mantidConfig["CheckMantidVersion.OnStartup"] = "0" |
| 57 | +mantidConfig["UpdateInstrumentDefinitions.OnStartup"] = "0" |
| 58 | +mantidConfig["usagereports.enabled"] = "0" |
| 59 | + |
| 60 | +####################################### |
| 61 | + |
| 62 | +# this at teardown removes the loggers, eliminating logger-related error printouts |
| 63 | +# see https://github.com/pytest-dev/pytest/issues/5502#issuecomment-647157873 |
| 64 | +@pytest.fixture(autouse=True, scope="session") |
| 65 | +def clear_loggers(): # noqa: PT004 |
| 66 | + """Remove handlers from all loggers""" |
| 67 | + import logging |
| 68 | + |
| 69 | + yield # ... teardown follows: |
| 70 | + loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values()) |
| 71 | + for logger in loggers: |
| 72 | + handlers = getattr(logger, "handlers", []) |
| 73 | + for handler in handlers: |
| 74 | + logger.removeHandler(handler) |
| 75 | + |
| 76 | +######################################################################################################################## |
| 77 | +# In combination, the following autouse fixtures allow unit tests and integration tests |
| 78 | +# to be run successfully without mocking out the `@Singleton` decorator. |
| 79 | +# |
| 80 | +# * The main objective is to allow the `@Singleton` classes to function as singletons, for the |
| 81 | +# duration of the single-test scope. This functionality is necessary, for example, in order for |
| 82 | +# the `Indexer` class to function correctly during the state-initialization sequence. |
| 83 | +# |
| 84 | +# * There are some fine points involved with using `@Singleton` classes during testing at class and module scope. |
| 85 | +# Such usage should probably be avoided whenever possible. It's a bit tricky to get this to work correctly |
| 86 | +# within the test framework. |
| 87 | +# |
| 88 | +# * TODO: Regardless of these fixtures, at the moment the `@Singleton` decorator must be completely turned ON during |
| 89 | +# integration tests, without any modification (e.g. or "reset"). There is something going on at "session" scope |
| 90 | +# with specific singletons not being deleted between tests, which results in multiple singleton instances when the |
| 91 | +# fixtures are used. This behavior does not seem to be an issue for the unit tests. |
| 92 | +# We can track this down by turning on the garbage collector `gc`, but this work has not yet been completed. |
| 93 | +# |
| 94 | +# Implementation notes: |
| 95 | +# |
| 96 | +# * Right now, there are > 36 `@Singleton` decorated classes. Probably, there should be far fewer. |
| 97 | +# Almost none of these classes are compute-intensive to initialize, or retain any cached data. |
| 98 | +# These would be the normal justifications for the use of this pattern. |
| 99 | +# |
| 100 | +# * Applying the `@Singleton` decorator changes the behavior of the classes, |
| 101 | +# so we don't want to mock the decorator out during testing. At present, the key class where this is important |
| 102 | +# is the `Indexer` class, which is not itself a singleton, but which is owned and cached |
| 103 | +# by the `LocalDataService` singleton. `Indexer` instances retain local data about indexing events |
| 104 | +# that have occurred since their initialization. |
| 105 | +# |
| 106 | + |
| 107 | +@pytest.fixture(autouse=True) |
| 108 | +def _reset_Singletons(request): |
| 109 | + if not "integration" in request.keywords: |
| 110 | + reset_Singletons() |
| 111 | + yield |
| 112 | + |
| 113 | +@pytest.fixture(scope="class", autouse=True) |
| 114 | +def _reset_class_scope_Singletons(request): |
| 115 | + if not "integration" in request.keywords: |
| 116 | + reset_Singletons() |
| 117 | + yield |
| 118 | + |
| 119 | +@pytest.fixture(scope="module", autouse=True) |
| 120 | +def _reset_module_scope_Singletons(request): |
| 121 | + if not "integration" in request.keywords: |
| 122 | + reset_Singletons() |
| 123 | + yield |
| 124 | + |
| 125 | +######################################################################################################################## |
| 126 | + |
| 127 | + |
| 128 | +## Import various `pytest.fixture` defined in separate `tests/util` modules: |
| 129 | +# ------------------------------------------------------------------------- |
| 130 | +# *** IMPORTANT WARNING: these must be included _after_ the `Singleton` decorator is patched ! *** |
| 131 | +# * Otherwise, the modules imported by these will not have the patched decorator applied to them. |
| 132 | + |
| 133 | +# from util.golden_data import goldenData, goldenDataFilePath |
| 134 | +# from util.state_helpers import state_root_fixture |
| 135 | +# from util.IPTS_override import IPTS_override_fixture |
| 136 | +from util.Config_helpers import Config_override_fixture |
| 137 | +from util.pytest_helpers import ( |
| 138 | + calibration_home_from_mirror, |
| 139 | + cleanup_workspace_at_exit, |
| 140 | + cleanup_class_workspace_at_exit, |
| 141 | + get_unique_timestamp, |
| 142 | + reduction_home_from_mirror |
| 143 | +) |
0 commit comments