github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
7.61 kB
# Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
# you can obtain one at http://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
# This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
"""Shared fixtures for backend tests.
The domain-specific packages (pypowsybl, expert_op4grid_recommender) are not
available in CI/test environments. We install lightweight mocks into
``sys.modules`` *before* any production module is imported so that collection
and import succeed without the real packages.
When the real packages *are* installed (e.g. local development), we prefer
them over mocks so that integration-style tests (TestRecommenderSimulationRealData)
can run against the real implementations.
"""
import sys
import importlib
from unittest.mock import MagicMock
# ---------------------------------------------------------------------------
# Mock heavy domain packages that are not available in test environments.
# Try to import each package first; only install a mock when the real package
# cannot be found.
# ---------------------------------------------------------------------------
_MOCK_MODULES = [
"pypowsybl",
"pypowsybl.network",
"pypowsybl.loadflow",
"pypowsybl_jupyter",
"pypowsybl_jupyter.util",
"expert_op4grid_recommender",
"expert_op4grid_recommender.config",
"expert_op4grid_recommender.main",
"expert_op4grid_recommender.data_loader",
"expert_op4grid_recommender.utils",
"expert_op4grid_recommender.utils.make_env_utils",
"expert_op4grid_recommender.action_evaluation",
"expert_op4grid_recommender.action_evaluation.classifier",
"expert_op4grid_recommender.environment_pypowsybl",
# utils.simulation is the single backend-agnostic simulation module since the
# recommender's R4 refactor unified the grid2op/pypowsybl pair (the old
# utils.simulation_pypowsybl was deleted).
"expert_op4grid_recommender.utils.simulation",
"expert_op4grid_recommender.environment",
"expert_op4grid_recommender.pypowsybl_backend",
"expert_op4grid_recommender.pypowsybl_backend.simulation_env",
"expert_op4grid_recommender.utils.superposition",
]
for mod_name in _MOCK_MODULES:
if mod_name not in sys.modules:
try:
importlib.import_module(mod_name)
except (ImportError, ModuleNotFoundError):
sys.modules[mod_name] = MagicMock()
# ---------------------------------------------------------------------------
# Ensure the mock config has the standard attributes the production code
# expects. ``from expert_op4grid_recommender import config`` resolves via
# sys.modules["expert_op4grid_recommender.config"], so we set them there.
# ---------------------------------------------------------------------------
_mock_config = sys.modules["expert_op4grid_recommender.config"]
_mock_config.MONITORING_FACTOR_THERMAL_LIMITS = 0.95
_mock_config.PRE_EXISTING_OVERLOAD_WORSENING_THRESHOLD = 0.02
_mock_config.IGNORE_LINES_MONITORING = True
_mock_config.PYPOWSYBL_FAST_MODE = True
_mock_config.MAX_RHO_BOTH_EXTREMITIES = True
_mock_config.CHECK_ACTION_SIMULATION = False
_mock_config.IGNORE_RECONNECTIONS = False
_mock_config.DATE = None
_mock_config.TIMESTEP = 0
_mock_config.MONITORED_LINES_COUNT = 0
_mock_config.DO_VISUALIZATION = True
_mock_config.USE_DC_LOAD_FLOW = False
from pathlib import Path as _Path
# Also set on the parent mock's attribute so both import paths work
sys.modules["expert_op4grid_recommender"].config = _mock_config
# ---------------------------------------------------------------------------
# Now it is safe to import production code
# ---------------------------------------------------------------------------
import pytest
import pandas as pd
import numpy as np
@pytest.fixture
def mock_network():
"""Create a mock pypowsybl network with realistic data."""
network = MagicMock()
network.id = "test_network"
lines_data = pd.DataFrame(
{
"voltage_level1_id": ["VL1", "VL1", "VL2"],
"voltage_level2_id": ["VL2", "VL3", "VL3"],
"i1": [100.0, 200.0, 150.0],
"i2": [95.0, 190.0, 148.0],
"p1": [50.0, 80.0, 60.0],
"p2": [-48.0, -78.0, -58.0],
},
index=["LINE_A", "LINE_B", "LINE_C"],
)
transformers_data = pd.DataFrame(
{
"voltage_level1_id": ["VL1", "VL3"],
"voltage_level2_id": ["VL4", "VL5"],
"i1": [300.0, 250.0],
"i2": [290.0, 245.0],
"p1": [120.0, 100.0],
"p2": [-118.0, -98.0],
},
index=["TRAFO_1", "TRAFO_2"],
)
voltage_levels_data = pd.DataFrame(
{"nominal_v": [400.0, 225.0, 90.0, 63.0, 20.0]},
index=["VL1", "VL2", "VL3", "VL4", "VL5"],
)
network.get_lines.return_value = lines_data
network.get_2_windings_transformers.return_value = transformers_data
network.get_voltage_levels.return_value = voltage_levels_data
return network
@pytest.fixture
def mock_network_service(mock_network):
"""Create a NetworkService with a pre-loaded mock network."""
from expert_backend.services.network_service import NetworkService
service = NetworkService()
service.network = mock_network
return service
# Only numeric/boolean config attributes need re-application after
# each test because patch.object may remove them. Path-like attributes
# (ENV_PATH, ACTION_FILE_PATH etc.) are left to MagicMock auto-creation
# so they don't trigger real filesystem access during tests.
_CONFIG_DEFAULTS = {
"MONITORING_FACTOR_THERMAL_LIMITS": 0.95,
"PRE_EXISTING_OVERLOAD_WORSENING_THRESHOLD": 0.02,
"IGNORE_LINES_MONITORING": True,
"PYPOWSYBL_FAST_MODE": True,
"MAX_RHO_BOTH_EXTREMITIES": True,
"CHECK_ACTION_SIMULATION": False,
"IGNORE_RECONNECTIONS": False,
"DATE": None,
"TIMESTEP": 0,
"MONITORED_LINES_COUNT": 0,
"DO_VISUALIZATION": True,
"USE_DC_LOAD_FLOW": False,
}
@pytest.fixture(autouse=True)
def reset_config():
"""Snapshot and restore the expert_op4grid_recommender.config state after each test."""
from expert_op4grid_recommender import config
# Snapshot all attributes that don't start with __
snapshot = {k: v for k, v in vars(config).items() if not k.startswith("__")}
yield
# Restore from snapshot
for k, v in snapshot.items():
setattr(config, k, v)
# Remove any attributes that were added during the test
current_keys = [k for k in vars(config).keys() if not k.startswith("__")]
for k in current_keys:
if k not in snapshot:
delattr(config, k)
# Re-apply standard defaults that the production code expects.
# Some tests (via patch.object) remove attributes during cleanup;
# this ensures every test starts with a consistent config state.
# Use vars(config) check to avoid overwriting values that were
# intentionally set by test modules (e.g. PYPOWSYBL_FAST_MODE = False).
config_dict = vars(config)
for k, v in _CONFIG_DEFAULTS.items():
if k not in config_dict:
setattr(config, k, v)
@pytest.fixture
def recommender_service_instance():
"""Create a fresh RecommenderService instance."""
from expert_backend.services.recommender_service import RecommenderService
return RecommenderService()