Skip to content

Commit

Permalink
Improve coverage in .report, .util
Browse files Browse the repository at this point in the history
  • Loading branch information
khaeru committed Oct 12, 2023
1 parent 3fda0b3 commit a5d3684
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 10 deletions.
2 changes: 1 addition & 1 deletion message_ix_models/project/ssp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def gen_structures(context, **kwargs):
["SSP-Review-Phase-1.csv.gz", "SspDb_country_data_2013-06-12.csv.zip"]
),
)
def make_test_data(filename):
def make_test_data(filename): # pragma: no cover
"""Create random data for testing."""
from pathlib import Path

Expand Down
3 changes: 3 additions & 0 deletions message_ix_models/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
from message_ix_models import Context, ScenarioInfo
from message_ix_models.util._logging import mark_time

from .config import Config

__all__ = [
"Config",
"prepare_reporter",
"register",
"report",
Expand Down
2 changes: 1 addition & 1 deletion message_ix_models/report/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def use_file(self, file_path: Union[str, Path, None]) -> None:
)
)
except StopIteration:
raise FileNotFoundError(f"Reporting configuration in {file_path}")
raise FileNotFoundError(f"Reporting configuration in '{file_path}(.yaml)'")

# Store for genno to handle
self.genno_config["path"] = path
Expand Down
1 change: 1 addition & 0 deletions message_ix_models/report/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,4 @@ class PrimaryEnergy1(FinalEnergy1):
def callback(c: Computer, context: "Context") -> None:
all_keys = [c.add(f"plot {p.basename}", p, "scenario") for p in PLOTS]
c.add("plot all", all_keys)
log.info(f"Add 'plot all' collecting {len(all_keys)} plots")
3 changes: 3 additions & 0 deletions message_ix_models/report/sim.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ def add_simulated_solution(
mark_time()
N = len(rep.graph)

# Ensure "scenario" is present in the graph
rep.graph.setdefault("scenario", None)

# Add simulated data
data = data or dict()
for name, item_info in SIMULATE_ITEMS.items():
Expand Down
10 changes: 10 additions & 0 deletions message_ix_models/tests/project/test_ssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ class TestSSPOriginal:
(
dict(measure="POP", model="OECD Env-Growth"),
dict(measure="GDP", model="OECD Env-Growth"),
# Excess keyword arguments
pytest.param(
dict(measure="GDP", model="OECD Env-Growth", foo="bar"),
marks=pytest.mark.xfail(raises=ValueError),
),
),
)
def test_prepare_computer(self, test_context, source, source_kw):
Expand Down Expand Up @@ -141,6 +146,11 @@ class TestSSPUpdate:
dict(measure="POP"),
dict(measure="GDP", model="IIASA GDP 2023"),
dict(measure="GDP", model="OECD ENV-Growth 2023"),
# Excess keyword arguments
pytest.param(
dict(measure="POP", foo="bar"),
marks=pytest.mark.xfail(raises=ValueError),
),
),
)
def test_prepare_computer(self, test_context, source, source_kw):
Expand Down
19 changes: 18 additions & 1 deletion message_ix_models/tests/report/test_computations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import re

import pandas as pd
import xarray as xr
from genno import Quantity

from message_ix_models.report.computations import compound_growth
from message_ix_models.report.computations import compound_growth, filter_ts


def test_compound_growth():
Expand All @@ -28,3 +31,17 @@ def test_compound_growth():
assert all(1.02**5 == r1.sel(t=2035) / r1.sel(t=2030))

assert all(1.0 == result.sel(x="x2"))


def test_filter_ts():
df = pd.DataFrame([["foo"], ["bar"]], columns=["variable"])
assert 2 == len(df)

# Operator runs
result = filter_ts(df, re.compile(".(ar)"))

# Only matching rows are returned
assert 1 == len(result)

# Only the first match group in `expr` is preserved
assert {"ar"} == set(result.variable.unique())
23 changes: 23 additions & 0 deletions message_ix_models/tests/report/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest

from message_ix_models.report.config import Config


class TestConfig:
def test_use_file(self, tmp_path):
cfg = Config()

# No effect
cfg.use_file(None)

# Passing a missing path raises an exception
with pytest.raises(
FileNotFoundError, match="Reporting configuration in .*missing"
):
cfg.use_file(tmp_path.joinpath("missing"))

# Passing a file name that does not exist raises an exception
with pytest.raises(
FileNotFoundError, match=r"Reporting configuration in 'unknown\(.yaml\)'"
):
cfg.use_file("unknown")
31 changes: 28 additions & 3 deletions message_ix_models/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ def test_apply_units(request, test_context, regions):
assert ["EUR_2005"] == df["unit"].unique()


@pytest.mark.xfail(reason="Incomplete", raises=TypeError)
def test_cli(mix_models_cli):
# TODO complete by providing a Scenario that is reportable (with solution)
mix_models_cli.assert_exit_0(["report"])


@pytest.mark.parametrize(
"input, exp",
(
Expand Down Expand Up @@ -203,8 +209,8 @@ def test_collapse(input, exp):
pdt.assert_frame_equal(util.collapse(df_in), df_exp)


@MARK[0]
def test_add_simulated_solution(test_context, test_data_path):
def ss_reporter():
"""Reporter with a simulated solution for snapshot 0."""
from message_ix import Reporter

rep = Reporter()
Expand All @@ -216,7 +222,15 @@ def test_add_simulated_solution(test_context, test_data_path):
path=package_data_path("test", "MESSAGEix-GLOBIOM_1.1_R11_no-policy_baseline"),
)

# out can be calculated using "output" and "ACT" from files in `path`
return rep


@MARK[0]
def test_add_simulated_solution(test_context, test_data_path):
# Simulated solution can be added to an empty Reporter
rep = ss_reporter()

# "out" can be calculated using "output" and "ACT" from files in `path`
result = rep.get("out:*")

# Has expected dimensions and length
Expand All @@ -237,3 +251,14 @@ def test_add_simulated_solution(test_context, test_data_path):
hd="year",
)
assert np.isclose(79.76478, value.item())


def test_prepare_reporter(test_context):
rep = ss_reporter()
N = len(rep.graph)

# prepare_reporter() works on the simulated solution
prepare_reporter(test_context, reporter=rep)

# A number of keys were added
assert 14299 <= len(rep.graph) - N
28 changes: 28 additions & 0 deletions message_ix_models/tests/util/test_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,31 @@ def func(ctx, ssp):

# The value was stored on, and retrieved from, `ctx`
assert "SSP2\n" == result.output


def test_urls_from_file(mix_models_cli, tmp_path):
"""Test :func:`.urls_from_file` callback."""

# Create a hidden command and attach it to the CLI
@click.command(name="_test_store_context", hidden=True)
@common_params("urls_from_file")
@click.pass_obj
def func(ctx, **kwargs):
# Print the value stored on the Context object
print("\n".join([s.url for s in ctx.core.scenarios]))

# Create a temporary file with some scenario URLs
text = """m/s#3
foo/bar#5
baz/qux#123
"""
p = tmp_path.joinpath("scenarios.txt")
p.write_text(text)

# Run the command, referring to the temporary file
with temporary_command(main, func):
result = mix_models_cli.assert_exit_0([func.name, f"--urls-from-file={p}"])

# Scenario URLs are parsed to ScenarioInfo objects, and then can be reconstructed →
# data is round-tripped
assert text == result.output
5 changes: 5 additions & 0 deletions message_ix_models/tests/util/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,8 @@ def test_replace(self, c):
result = c.replace(foo_2="baz")
assert result is not c
assert "baz" == result.foo_2

def test_update(self, c):
""":meth:`.update` raises AttributeError."""
with pytest.raises(AttributeError):
c.update(foo_4="")
6 changes: 6 additions & 0 deletions message_ix_models/tests/util/test_scenarioinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ def test_from_scenario(self, test_context) -> None:
assert 1963 == info.y0
assert [1963, 1964, 1965] == info.Y

def test_from_url(self):
si = ScenarioInfo.from_url("m/s#123")
assert "m" == si.model
assert "s" == si.scenario
assert 123 == si.version

@pytest.mark.parametrize(
"input, expected",
(
Expand Down
12 changes: 9 additions & 3 deletions message_ix_models/util/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional, Union
from typing import List, Optional, Union

import click
from click import Argument, Choice, Option
Expand Down Expand Up @@ -86,9 +86,15 @@ def store_context(context: Union[click.Context, Context], param, value):
return value


def urls_from_file(context: Union[click.Context, Context], param, value):
def urls_from_file(
context: Union[click.Context, Context], param, value
) -> List[ScenarioInfo]:
"""Callback to parse scenario URLs from `value`."""
si = []
si: List[ScenarioInfo] = []

if value is None:
return si

with click.open_file(value) as f:
for line in f:
si.append(ScenarioInfo.from_url(url=line))
Expand Down
7 changes: 7 additions & 0 deletions message_ix_models/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ def replace(self, **kwargs):
)

def update(self, **kwargs):
"""Update attributes in-place.
Raises
------
AttributeError
Any of the `kwargs` are not fields in the data class.
"""
# TODO use _munge_dict(); allow a positional argument
for k, v in kwargs.items():
if not hasattr(self, k):
Expand Down
2 changes: 1 addition & 1 deletion message_ix_models/util/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def only(cls) -> "Context":

def __init__(self, *args, **kwargs):
from message_ix_models.model import Config as ModelConfig
from message_ix_models.report.config import Config as ReportConfig
from message_ix_models.report import Config as ReportConfig

if len(_CONTEXTS) == 0:
log.info("Create root Context")
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ exclude_also = [
"@(abc\\.)?abstractmethod",
# Imports only used by type checkers
"if TYPE_CHECKING:",
# Requires message_data
"if HAS_MESSAGE_DATA:",
]

[tool.mypy]
Expand Down

0 comments on commit a5d3684

Please sign in to comment.