Skip to content

Commit

Permalink
✨ feat: Parametrize test_mp and enable IXMPBackend.__init__
Browse files Browse the repository at this point in the history
  • Loading branch information
glatterf42 committed Jan 20, 2025
1 parent 7bfafe4 commit 0ae6398
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 49 deletions.
28 changes: 14 additions & 14 deletions ixmp/backend/ixmp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,34 @@ class IXMP4Backend(CachingBackend):

_platform: "ixmp4.Platform"

def __init__(self):
import ixmp4
def __init__(self, **kwargs) -> None:
from ixmp4 import Platform
from ixmp4.core.exceptions import PlatformNotFound

# TODO Obtain `name` from the ixmp.Platform creating this Backend
name = "test"
# TODO Handle errors or make sure name is always present for this backend
name = kwargs.pop("name")

# Add an ixmp4.Platform using ixmp4's own configuration code
# TODO Move this to a test fixture
# NB ixmp.tests.conftest.test_sqlite_mp exists, but is not importable (missing
# __init__.py)
# NB test_platform is parametrized for both backends, but
# TestPlatform::test_init1 calls this function without defining an
# ixmp4.Platform first
import ixmp4.conf

dsn = "sqlite:///:memory:"
try:
ixmp4.conf.settings.toml.get_platform(name)
except PlatformNotFound:
# TODO Handle errors or make sure dsn is always present when the platform is
# not known
dsn = kwargs.pop("dsn")
ixmp4.conf.settings.toml.add_platform(name, dsn)
except ixmp4.core.exceptions.PlatformNotUnique:
pass

# Instantiate and store
self._platform = ixmp4.Platform(name)
self._platform = Platform(name)

def get_scenarios(self, default, model, scenario):
# Current fails with:
# sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: run
# [SQL: SELECT DISTINCT run.model__id, run.scenario__id, run.version,
# run.is_default, run.id
# FROM run
# WHERE run.is_default = 1 ORDER BY run.id ASC]
return self._platform.runs.list()

# The below methods of base.Backend are not yet implemented
Expand Down
3 changes: 3 additions & 0 deletions ixmp/core/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(
# Overwrite any platform config with explicit keyword arguments
kwargs.update(backend_args)

if backend == "ixmp4":
kwargs["name"] = self.name

# Retrieve the Backend class
try:
backend_class_name = kwargs.pop("class")
Expand Down
79 changes: 59 additions & 20 deletions ixmp/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,20 @@
import logging
import os
import shutil
from collections.abc import Generator
from contextlib import contextmanager, nullcontext
from copy import deepcopy
from itertools import chain
from pathlib import Path
from typing import Any

import pint
import pytest
from click.testing import CliRunner
from ixmp4.conf.base import PlatformInfo
from ixmp4.data.backend import SqliteTestBackend

from ixmp import Platform, cli
from ixmp import BACKENDS, Platform, cli
from ixmp import config as ixmp_config

from .data import (
Expand Down Expand Up @@ -89,7 +93,7 @@
# Pytest hooks


def pytest_addoption(parser):
def pytest_addoption(parser: pytest.Parser) -> None:
"""Add the ``--user-config`` command-line option to pytest."""
parser.addoption(
"--ixmp-jvm-mem",
Expand All @@ -103,7 +107,7 @@ def pytest_addoption(parser):
)


def pytest_sessionstart(session):
def pytest_sessionstart(session: pytest.Session) -> None:
"""Unset any configuration read from the user's directory."""
from ixmp.backend import jdbc

Expand All @@ -117,7 +121,7 @@ def pytest_sessionstart(session):
jdbc._GC_AGGRESSIVE = False


def pytest_report_header(config, start_path):
def pytest_report_header(config, start_path) -> str:
"""Add the ixmp configuration to the pytest report header."""
return f"ixmp config: {repr(ixmp_config.values)}"

Expand All @@ -137,7 +141,7 @@ def invoke(self, *args, **kwargs):


@pytest.fixture(scope="module")
def mp(test_mp):
def mp(test_mp: Platform) -> Generator[Platform, Any, None]:
"""A :class:`.Platform` containing test data.
This fixture is **module** -scoped, and is used in :mod:`.test_platform`,
Expand All @@ -149,23 +153,29 @@ def mp(test_mp):


@pytest.fixture(scope="session")
def test_data_path():
def test_data_path() -> Path:
"""Path to the directory containing test data."""
return Path(__file__).parents[1].joinpath("tests", "data")


@pytest.fixture(scope="module")
def test_mp(request, tmp_env, test_data_path):
@pytest.fixture(scope="module", params=list(BACKENDS.keys()))
def test_mp(
request: pytest.FixtureRequest, tmp_env, test_data_path
) -> Generator[Platform, Any, None]:
"""An empty :class:`.Platform` connected to a temporary, in-memory database.
This fixture has **module** scope: the same Platform is reused for all tests in a
module.
"""
yield from _platform_fixture(request, tmp_env, test_data_path)
yield from _platform_fixture(
request, tmp_env, test_data_path, backend=request.param
)


@pytest.fixture(scope="session")
def tmp_env(pytestconfig, tmp_path_factory):
def tmp_env(
pytestconfig: pytest.Config, tmp_path_factory: pytest.TempPathFactory
) -> Generator[os._Environ[str], Any, None]:
"""Return the os.environ dict with the IXMP_DATA variable set.
IXMP_DATA will point to a temporary directory that is unique to the test session.
Expand All @@ -187,13 +197,13 @@ def tmp_env(pytestconfig, tmp_path_factory):


@pytest.fixture(scope="session")
def tutorial_path():
def tutorial_path() -> Path:
"""Path to the directory containing the tutorials."""
return Path(__file__).parents[2].joinpath("tutorial")


@pytest.fixture(scope="session")
def ureg():
def ureg() -> Generator[pint.UnitRegistry, Any, None]:
"""Application-wide units registry."""
registry = pint.get_application_registry()

Expand Down Expand Up @@ -242,8 +252,8 @@ def protect_rename_dims():
RENAME_DIMS.update(saved)


@pytest.fixture(scope="function")
def test_mp_f(request, tmp_env, test_data_path):
@pytest.fixture(scope="function", params=list(BACKENDS.keys()))
def test_mp_f(request: pytest.FixtureRequest, tmp_env, test_data_path):
"""An empty :class:`Platform` connected to a temporary, in-memory database.
This fixture has **function** scope: the same Platform is reused for one test
Expand All @@ -253,7 +263,9 @@ def test_mp_f(request, tmp_env, test_data_path):
--------
test_mp
"""
yield from _platform_fixture(request, tmp_env, test_data_path)
yield from _platform_fixture(
request, tmp_env, test_data_path, backend=request.param
)


# Assertions
Expand Down Expand Up @@ -384,19 +396,38 @@ def create_test_platform(tmp_path, data_path, name, **properties):
# Private utilities


def _platform_fixture(request, tmp_env, test_data_path):
def _platform_fixture(
request: pytest.FixtureRequest, tmp_env, test_data_path, backend: str
) -> Generator[Platform, Any, None]:
"""Helper for :func:`test_mp` and other fixtures."""
# Long, unique name for the platform.
# Remove '/' so that the name can be used in URL tests.
platform_name = request.node.nodeid.replace("/", " ")

# Add a platform
ixmp_config.add_platform(
platform_name, "jdbc", "hsqldb", url=f"jdbc:hsqldb:mem:{platform_name}"
)
if backend == "jdbc":
ixmp_config.add_platform(
platform_name, backend, "hsqldb", url=f"jdbc:hsqldb:mem:{platform_name}"
)
elif backend == "ixmp4":
import ixmp4.conf

# Setup ixmp4 backend and run DB migrations
sqlite = SqliteTestBackend(
PlatformInfo(name=platform_name, dsn="sqlite:///:memory:")
)
sqlite.setup()

# Add DB to ixmp4 config
ixmp4.conf.settings.toml.add_platform(
name=platform_name, dsn="sqlite:///:memory:"
)

# Add ixmp4 backend to ixmp platforms
ixmp_config.add_platform(platform_name, backend)

# Launch Platform
mp = Platform(name=platform_name)
mp = Platform(name=platform_name, backend=backend)
yield mp

# Teardown: don't show log messages when destroying the platform, even if
Expand All @@ -406,3 +437,11 @@ def _platform_fixture(request, tmp_env, test_data_path):

# Remove from config
ixmp_config.remove_platform(platform_name)

if backend == "ixmp4":
assert sqlite # to satisfy type checkers

# Close DB connection and remove platform
sqlite.close()
sqlite.teardown()
ixmp4.conf.settings.toml.remove_platform(platform_name)
2 changes: 1 addition & 1 deletion ixmp/testing/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def make_dantzig(
return scen


def populate_test_platform(platform):
def populate_test_platform(platform: Platform) -> None:
"""Populate `platform` with data for testing.
Many of the tests in :mod:`ixmp.tests.core` depend on this set of data.
Expand Down
18 changes: 4 additions & 14 deletions ixmp/tests/core/test_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import re
from sys import getrefcount
from typing import TYPE_CHECKING, Generator
from typing import TYPE_CHECKING
from weakref import getweakrefcount

import pandas as pd
Expand All @@ -20,17 +20,6 @@


class TestPlatform:
@pytest.fixture(params=list(ixmp.BACKENDS))
def mp(self, request, test_mp) -> Generator[ixmp.Platform, None, None]:
"""Fixture that yields 2 different platforms: one JDBC-backed, one ixmp4."""
backend = request.param

if backend == "jdbc":
yield test_mp
elif backend == "ixmp4":
# TODO Use a fixture similar to test_mp (with same contents) backed by ixmp4
yield ixmp.Platform(backend="ixmp4")

def test_init0(self):
with pytest.raises(
ValueError,
Expand All @@ -46,7 +35,8 @@ def test_init0(self):
"backend, backend_args",
(
("jdbc", dict(driver="hsqldb", url="jdbc:hsqldb:mem:TestPlatform")),
("ixmp4", dict()),
# TODO use this/default name for ixmp4 platforms without passing it manually
("ixmp4", dict(name="ixmp4-local")),
),
)
def test_init1(self, backend, backend_args):
Expand All @@ -58,7 +48,7 @@ def test_getattr(self, test_mp):
with pytest.raises(AttributeError):
test_mp.not_a_direct_backend_method

def test_scenario_list(self, mp):
def test_scenario_list(self, mp: ixmp.Platform) -> None:
scenario = mp.scenario_list()
assert isinstance(scenario, pd.DataFrame)

Expand Down

0 comments on commit 0ae6398

Please sign in to comment.