Source code for credmark.cmf.engine.model_unittest

import logging
import unittest
from typing import Union

from dotenv import find_dotenv, load_dotenv

from credmark.cmf.engine.cache import ModelRunCache
from credmark.cmf.engine.context import EngineModelContext
from credmark.cmf.engine.mocks import ModelMockConfig, ModelMockRunner
from credmark.cmf.engine.model_api import ModelApi
from credmark.cmf.engine.model_loader import ModelLoader
from credmark.cmf.engine.web3.registry import Web3Registry
from credmark.cmf.model.context import ModelContext


class ModelTestContextFactory:
    # Helps with configuring unittest mocks and contexts
    # A ModelTestContextFactory instance can be configured at startup
    # and set by calling use_factory().

    _factory: Union['ModelTestContextFactory', None] = None

    @classmethod
    def use_factory(cls, model_test_factory: 'ModelTestContextFactory'):
        cls._factory = model_test_factory

    @classmethod
    def factory(cls):
        if cls._factory is None:
            cls._factory = cls.create_default_factory()
        return cls._factory

    @classmethod
    def create_default_factory(cls):
        load_dotenv(find_dotenv('.env.test', usecwd=True))
        chain_to_provider_url = Web3Registry.load_providers_from_env()
        model_loader = ModelLoader(['models', 'tests'], load_dev_models=True)
        return ModelTestContextFactory(model_loader, chain_to_provider_url)

    def __init__(self,
                 model_loader: Union[ModelLoader, None] = None,
                 chain_to_provider_url: Union[dict[str, str], None] = None,
                 api_url: Union[str, None] = None):

        if model_loader is None:
            model_loader = ModelLoader(['.'])

        self.model_loader = model_loader
        self.api = ModelApi.api_for_url(api_url)

        self.web3_registry = Web3Registry(chain_to_provider_url)

        EngineModelContext.dev_mode = True
        EngineModelContext.test_mode = True

        EngineModelContext.use_local_models_slugs.update(
            model_loader.loaded_dev_model_slugs())

    def create_context(self,
                       chain_id: int = 1,
                       block_number: int = 17_222_851):
        # Clear the current context first
        ModelContext.set_current_context(None)

        model_cache = ModelRunCache(enabled=False)

        context = EngineModelContext(
            chain_id, block_number, self.web3_registry,
            'test', 0, self.model_loader, model_cache,
            self.api, is_top_level=True)

        context.__dict__['original_input'] = {}
        context.__dict__['slug'] = 'test'

        ModelContext.set_current_context(context)

        return context

    def clear_context(self):
        ModelContext.set_current_context(None)

    def use_mocks(self, mocks: Union[ModelMockConfig, None] = None):
        if mocks is not None:
            mock_runner = ModelMockRunner()
            mock_runner.add_mock_configuration(mocks)
            EngineModelContext.use_model_mock_runner(mock_runner)
        else:
            EngineModelContext.use_model_mock_runner(None)


[docs]def model_context(chain_id: int = 1, block_number: int = 17_222_851, mocks: Union[ModelMockConfig, None] = None): """ A decorator that can be used on a test method in a ModelTestCase subclass to configure the context and mocks to use during the test. Example:: @model_context(block_number=5000) def test_model(self): # self.context.block_number == 5000 """ def _deco(func): def _wrapper(self, *args, **kwargs): factory = ModelTestContextFactory.factory() factory.use_mocks(mocks) self.context = factory.create_context(chain_id, block_number) self.logger.debug('%s.%s using context chain_id=%d block_number=%d' % (self.__class__.__name__, func.__name__, chain_id, block_number)) return func(self, *args, **kwargs) return _wrapper return _deco
[docs]class ModelTestCase(unittest.TestCase): """ A superclass for unittest TestCase instances that use framework classes and call other models. A ModelTestCase has a default context defined during the tests which is available at ``self.context``. To configure a specific context and mocks for a test, it is recommended to use the :func:`~credmark.cmf.engine.model_unittest.model_context` decorator on test_methods. Example:: @model_context(block_number=5000) def test_model(self): # self.context.block_number == 5000 Alternatively, a test method can use the ``self.create_model_context()`` to create a new context and set mocks during a test. """ def __init__(self, methodName='runTest'): super().__init__(methodName) self.logger = logging.getLogger(self.__class__.__name__) self.logger.setLevel(logging.DEBUG) def setUp(self): super().setUp() self.create_model_context() def tearDown(self) -> None: ModelTestContextFactory.factory().clear_context() return super().tearDown()
[docs] def create_model_context(self, chain_id: int = 1, block_number: int = 17_222_851, mocks: Union[ModelMockConfig, None] = None): """ Create a new model context and set it as the current context. """ factory = ModelTestContextFactory.factory() factory.use_mocks(mocks) self.context = factory.create_context(chain_id, block_number) self.logger.debug('%s using context chain_id=%d block_number=%d' % (self.__class__.__name__, chain_id, block_number))
@property def context(self): """ Gets the context. If it doesn't exist, a default context will be created. Use the :func:`~credmark.cmf.engine.model_unittest.model_context` decorator or call ``self.create_model_context()`` to configure a context. """ if self.__context is None: self.create_model_context() return self.__context @context.setter def context(self, value): self.__context = value