Coverage for tests/test_results.py: 99%
327 statements
« prev ^ index » next coverage.py v7.4.4, created at 2026-03-26 09:43 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2026-03-26 09:43 +0000
1"""
2Tests for the Results class.
4This test module verifies the Results class functionality for loading and managing
5flexibility analysis results.
7Run with: pytest test_results.py -v
8"""
10import copy
11import json
12import tempfile
13from pathlib import Path
14from unittest.mock import MagicMock
15import sys
16import pandas as pd
17import pytest
19from agentlib_flexquant.data_structures.flex_results import (
20 Results,
21 load_indicator,
22 load_market,
23)
26# =============================================================================
27# Path configuration
28# =============================================================================
30SAMPLE_FILES_DIR = Path(__file__).parent / "sample_files"
31CONFIGS_DIR = SAMPLE_FILES_DIR / "configs"
32RESULTS_DIR = SAMPLE_FILES_DIR / "sample_results"
34# Config file paths, test for both flex configs (the one used as input for the
35# FlexAgentGenerator and the one created by it)
36FLEX_CONFIG_PATH = CONFIGS_DIR / "flexibility_agent_config.json"
37FLEX_CONFIG_INPUT_PATH = CONFIGS_DIR / "flexibility_agent_config_input.json"
38BASELINE_CONFIG_PATH = CONFIGS_DIR / "baseline.json"
39POS_FLEX_CONFIG_PATH = CONFIGS_DIR / "pos_flex.json"
40NEG_FLEX_CONFIG_PATH = CONFIGS_DIR / "neg_flex.json"
41INDICATOR_CONFIG_PATH = CONFIGS_DIR / "indicator.json"
42SIMULATOR_CONFIG_PATH = CONFIGS_DIR / "simulator.json"
44# Result file paths
45MPC_BASE_PATH = RESULTS_DIR / "mpc_base.csv"
46MPC_POS_FLEX_PATH = RESULTS_DIR / "mpc_pos_flex.csv"
47MPC_NEG_FLEX_PATH = RESULTS_DIR / "mpc_neg_flex.csv"
48STATS_MPC_BASE_PATH = RESULTS_DIR / "stats_mpc_base.csv"
49STATS_MPC_POS_FLEX_PATH = RESULTS_DIR / "stats_mpc_pos_flex.csv"
50STATS_MPC_NEG_FLEX_PATH = RESULTS_DIR / "stats_mpc_neg_flex.csv"
51INDICATOR_RESULTS_PATH = RESULTS_DIR / "flexibility_indicator.csv"
52MARKET_RESULTS_PATH = RESULTS_DIR / "flexibility_market.csv"
53SIMULATOR_RESULTS_PATH = RESULTS_DIR / "simulator.csv"
56# =============================================================================
57# Fixtures
58# =============================================================================
61@pytest.fixture(params=[FLEX_CONFIG_PATH, FLEX_CONFIG_INPUT_PATH], ids=["flex_config", "flex_config_input"])
62def flex_config_path(request):
63 """Return path to flex config (parameterized for both config variants)."""
64 return request.param
67@pytest.fixture(params=[FLEX_CONFIG_PATH, FLEX_CONFIG_INPUT_PATH], ids=["flex_config", "flex_config_input"])
68def flex_config_dict(request):
69 """Load and return flex config as dict (parameterized for both config variants)."""
70 with open(request.param, "r", encoding="utf-8") as f:
71 return json.load(f)
74@pytest.fixture
75def simulator_config_path():
76 """Return path to simulator config."""
77 return SIMULATOR_CONFIG_PATH
80@pytest.fixture
81def simulator_config_dict():
82 """Load and return simulator config as dict."""
83 with open(SIMULATOR_CONFIG_PATH, "r", encoding="utf-8") as f:
84 return json.load(f)
87@pytest.fixture
88def results_instance(flex_config_path, simulator_config_path):
89 """Create a fully initialized Results instance."""
90 return Results(
91 flex_config=flex_config_path,
92 simulator_agent_config=simulator_config_path,
93 generated_flex_files_base_path=SAMPLE_FILES_DIR,
94 results=RESULTS_DIR,
95 to_timescale="seconds",
96 )
99@pytest.fixture
100def results_instance_no_simulator(flex_config_path):
101 """Create a Results instance without simulator config."""
102 return Results(
103 flex_config=flex_config_path,
104 simulator_agent_config=None,
105 generated_flex_files_base_path=SAMPLE_FILES_DIR,
106 results=RESULTS_DIR,
107 to_timescale="seconds",
108 )
111@pytest.fixture
112def temp_dir():
113 """Create a temporary directory for test files."""
114 with tempfile.TemporaryDirectory() as tmpdir:
115 yield Path(tmpdir)
118# =============================================================================
119# Tests for standalone loading functions
120# =============================================================================
123class TestLoadIndicator:
124 """Tests for the load_indicator function."""
126 def test_load_indicator_returns_dataframe(self):
127 """Test that load_indicator returns a DataFrame."""
128 df = load_indicator(INDICATOR_RESULTS_PATH)
129 assert isinstance(df, pd.DataFrame)
131 def test_load_indicator_has_multiindex(self):
132 """Test that loaded DataFrame has MultiIndex."""
133 df = load_indicator(INDICATOR_RESULTS_PATH)
134 assert isinstance(df.index, pd.MultiIndex)
135 assert df.index.nlevels == 2
137 def test_load_indicator_not_empty(self):
138 """Test that loaded DataFrame is not empty."""
139 df = load_indicator(INDICATOR_RESULTS_PATH)
140 assert len(df) > 0
142 def test_load_indicator_with_string_path(self):
143 """Test loading with string path instead of Path object."""
144 df = load_indicator(str(INDICATOR_RESULTS_PATH))
145 assert isinstance(df, pd.DataFrame)
148class TestLoadMarket:
149 """Tests for the load_market function."""
151 def test_load_market_returns_dataframe(self):
152 """Test that load_market returns a DataFrame."""
153 df = load_market(MARKET_RESULTS_PATH)
154 assert isinstance(df, pd.DataFrame)
156 def test_load_market_has_multiindex(self):
157 """Test that loaded DataFrame has MultiIndex."""
158 df = load_market(MARKET_RESULTS_PATH)
159 assert isinstance(df.index, pd.MultiIndex)
160 assert df.index.nlevels == 2
162 def test_load_market_not_empty(self):
163 """Test that loaded DataFrame is not empty."""
164 df = load_market(MARKET_RESULTS_PATH)
165 assert len(df) > 0
167 def test_load_market_with_string_path(self):
168 """Test loading with string path instead of Path object."""
169 df = load_market(str(MARKET_RESULTS_PATH))
170 assert isinstance(df, pd.DataFrame)
173# =============================================================================
174# Tests for Results class initialization
175# =============================================================================
178class TestResultsInit:
179 """Tests for Results class initialization."""
181 def test_init_with_all_configs(self, flex_config_path, simulator_config_path):
182 """Test initialization with all configurations provided."""
183 results = Results(
184 flex_config=flex_config_path,
185 simulator_agent_config=simulator_config_path,
186 generated_flex_files_base_path=SAMPLE_FILES_DIR,
187 results=RESULTS_DIR,
188 to_timescale="seconds",
189 )
190 assert results is not None
191 assert hasattr(results, "df_baseline")
192 assert hasattr(results, "df_pos_flex")
193 assert hasattr(results, "df_neg_flex")
194 assert hasattr(results, "df_indicator")
196 def test_init_without_simulator_config(self, flex_config_path):
197 """Test initialization without simulator agent config."""
198 results = Results(
199 flex_config=flex_config_path,
200 simulator_agent_config=None,
201 generated_flex_files_base_path=SAMPLE_FILES_DIR,
202 results=RESULTS_DIR,
203 to_timescale="seconds",
204 )
205 assert results is not None
206 assert not hasattr(results, "df_simulation") or results.df_simulation is None
208 def test_init_with_dict_flex_config(self, flex_config_dict, simulator_config_path):
209 """Test initialization with flex_config as dict."""
210 results = Results(
211 flex_config=flex_config_dict,
212 simulator_agent_config=simulator_config_path,
213 generated_flex_files_base_path=SAMPLE_FILES_DIR,
214 results=RESULTS_DIR,
215 to_timescale="seconds",
216 )
217 assert results is not None
219 def test_init_with_dict_simulator_config(self, flex_config_path, simulator_config_dict):
220 """Test initialization with simulator_agent_config as dict."""
221 results = Results(
222 flex_config=flex_config_path,
223 simulator_agent_config=simulator_config_dict,
224 generated_flex_files_base_path=SAMPLE_FILES_DIR,
225 results=RESULTS_DIR,
226 to_timescale="seconds",
227 )
228 assert results is not None
229 assert hasattr(results, "df_simulation")
231 def test_init_with_string_paths(self, flex_config_path):
232 """Test initialization with string paths instead of Path objects."""
233 results = Results(
234 flex_config=str(flex_config_path),
235 simulator_agent_config=str(SIMULATOR_CONFIG_PATH),
236 generated_flex_files_base_path=str(SAMPLE_FILES_DIR),
237 results=str(RESULTS_DIR),
238 to_timescale="seconds",
239 )
240 assert results is not None
242 def test_init_from_existing_results_instance(self, results_instance):
243 """Test that Results can be initialized from another Results instance."""
244 new_results = Results(
245 flex_config=None,
246 simulator_agent_config=None,
247 results=results_instance,
248 )
249 assert new_results is not None
250 assert new_results.df_baseline is not None
251 # Verify it's a deep copy
252 assert new_results is not results_instance
254 def test_init_with_different_timescales(self, flex_config_path, simulator_config_path):
255 """Test initialization with different timescale options."""
256 for timescale in ["seconds", "minutes", "hours"]:
257 results = Results(
258 flex_config=flex_config_path,
259 simulator_agent_config=simulator_config_path,
260 generated_flex_files_base_path=SAMPLE_FILES_DIR,
261 results=RESULTS_DIR,
262 to_timescale=timescale,
263 )
264 assert results.current_timescale_of_data == timescale
267# =============================================================================
268# Tests for _load_flex_config
269# =============================================================================
272class TestLoadFlexConfig:
273 """Tests for the _load_flex_config method."""
275 def test_load_flex_config_from_path(self, flex_config_path):
276 """Test loading flex config from file path."""
277 results = Results(
278 flex_config=flex_config_path,
279 simulator_agent_config=None,
280 generated_flex_files_base_path=SAMPLE_FILES_DIR,
281 results=RESULTS_DIR,
282 )
283 assert results.flex_config is not None
285 def test_load_flex_config_from_dict(self, flex_config_dict):
286 """Test loading flex config from dict."""
287 results = Results(
288 flex_config=flex_config_dict,
289 simulator_agent_config=None,
290 generated_flex_files_base_path=SAMPLE_FILES_DIR,
291 results=RESULTS_DIR,
292 )
293 assert results.flex_config is not None
295 def test_load_flex_config_with_custom_base_path_overrides(
296 self, flex_config_path, temp_dir
297 ):
298 """Test that custom_base_path overrides flex_base_directory_path."""
299 # Copy necessary files to temp_dir for this test
300 # The custom base path should override what's in the config
301 with open(flex_config_path, "r", encoding="utf-8") as f:
302 config = json.load(f)
304 original_base_path = config.get("flex_base_directory_path")
306 results = Results(
307 flex_config=flex_config_path,
308 simulator_agent_config=None,
309 generated_flex_files_base_path=SAMPLE_FILES_DIR,
310 results=RESULTS_DIR,
311 )
313 # The generator config should have the overridden path
314 assert str(results.flex_config.flex_base_directory_path) == str(
315 SAMPLE_FILES_DIR
316 )
319# =============================================================================
320# Tests for _load_simulator_config
321# =============================================================================
324class TestLoadSimulatorConfig:
325 """Tests for the _load_simulator_config method."""
327 def test_load_simulator_config_from_path(
328 self, flex_config_path, simulator_config_path
329 ):
330 """Test loading simulator config from file path."""
331 results = Results(
332 flex_config=flex_config_path,
333 simulator_agent_config=simulator_config_path,
334 generated_flex_files_base_path=SAMPLE_FILES_DIR,
335 results=RESULTS_DIR,
336 )
337 assert results.simulator_agent_config is not None
338 assert results.simulator_module_config is not None
340 def test_load_simulator_config_from_dict(
341 self, flex_config_path, simulator_config_dict
342 ):
343 """Test loading simulator config from dict."""
344 results = Results(
345 flex_config=flex_config_path,
346 simulator_agent_config=simulator_config_dict,
347 generated_flex_files_base_path=SAMPLE_FILES_DIR,
348 results=RESULTS_DIR,
349 )
350 assert results.simulator_agent_config is not None
351 assert results.simulator_module_config is not None
353 def test_simulator_module_has_result_filename(
354 self, flex_config_path, simulator_config_path
355 ):
356 """Test that simulator module config has result_filename attribute."""
357 results = Results(
358 flex_config=flex_config_path,
359 simulator_agent_config=simulator_config_path,
360 generated_flex_files_base_path=SAMPLE_FILES_DIR,
361 results=RESULTS_DIR,
362 )
363 assert hasattr(results.simulator_module_config, "result_filename")
366# =============================================================================
367# Tests for _resolve_sim_results_path
368# =============================================================================
371class TestResolveSimResultsPath:
372 """Tests for the _resolve_sim_results_path method."""
374 def test_resolve_absolute_path_exists(self, results_instance):
375 """Test resolution when absolute path exists."""
376 resolved = results_instance._resolve_sim_results_path(
377 str(SIMULATOR_RESULTS_PATH.absolute()), RESULTS_DIR
378 )
379 assert resolved.exists()
381 def test_resolve_filename_in_results_dir(self, results_instance):
382 """Test resolution when only filename given and file is in results dir."""
383 resolved = results_instance._resolve_sim_results_path(
384 "simulator.csv", RESULTS_DIR
385 )
386 assert resolved.exists()
387 assert resolved.name == "simulator.csv"
389 def test_resolve_file_not_found_raises(self, results_instance):
390 """Test that FileNotFoundError is raised when file cannot be found."""
391 with pytest.raises(
392 FileNotFoundError, match="Could not locate simulator results file"
393 ):
394 results_instance._resolve_sim_results_path(
395 "nonexistent_file.csv", RESULTS_DIR
396 )
398 def test_resolve_with_path_object(self, results_instance):
399 """Test resolution with Path object instead of string."""
400 resolved = results_instance._resolve_sim_results_path(
401 Path("simulator.csv"), RESULTS_DIR
402 )
403 assert resolved.exists()
406# =============================================================================
407# Tests for _load_results
408# =============================================================================
411class TestLoadResults:
412 """Tests for the _load_results method."""
414 def test_load_results_returns_tuple(self, results_instance):
415 """Test that _load_results returns a tuple of (dict, path)."""
416 # We can't easily call _load_results directly after init,
417 # but we can verify the results were loaded correctly
418 assert results_instance.df_baseline is not None
419 assert results_instance.df_pos_flex is not None
420 assert results_instance.df_neg_flex is not None
421 assert results_instance.df_indicator is not None
423 def test_load_results_with_path_string(self, flex_config_path, simulator_config_path):
424 """Test loading results with string path."""
425 results = Results(
426 flex_config=flex_config_path,
427 simulator_agent_config=simulator_config_path,
428 generated_flex_files_base_path=SAMPLE_FILES_DIR,
429 results=str(RESULTS_DIR),
430 to_timescale="seconds",
431 )
432 assert results.df_baseline is not None
435# =============================================================================
436# Tests for _load_results_dataframes
437# =============================================================================
440class TestLoadResultsDataframes:
441 """Tests for the _load_results_dataframes method."""
443 def test_dataframes_are_pandas_dataframes(self, results_instance):
444 """Test that all loaded results are pandas DataFrames."""
445 assert isinstance(results_instance.df_baseline, pd.DataFrame)
446 assert isinstance(results_instance.df_pos_flex, pd.DataFrame)
447 assert isinstance(results_instance.df_neg_flex, pd.DataFrame)
448 assert isinstance(results_instance.df_indicator, pd.DataFrame)
450 def test_dataframes_not_empty(self, results_instance):
451 """Test that loaded DataFrames are not empty."""
452 assert len(results_instance.df_baseline) > 0
453 assert len(results_instance.df_pos_flex) > 0
454 assert len(results_instance.df_neg_flex) > 0
455 assert len(results_instance.df_indicator) > 0
457 def test_simulation_dataframe_loaded_when_simulator_present(self, results_instance):
458 """Test that df_simulation is loaded when simulator config is present."""
459 assert hasattr(results_instance, "df_simulation")
460 assert isinstance(results_instance.df_simulation, pd.DataFrame)
461 assert len(results_instance.df_simulation) > 0
463 def test_simulation_dataframe_not_present_without_simulator(
464 self, results_instance_no_simulator
465 ):
466 """Test that df_simulation is not set when simulator config is absent."""
467 assert not hasattr(results_instance_no_simulator, "df_simulation")
469 def test_market_dataframe_loaded_when_market_config_present(self, results_instance):
470 """Test that df_market is loaded when market config is present."""
471 if results_instance.flex_config.market_config:
472 assert results_instance.df_market is not None
473 assert isinstance(results_instance.df_market, pd.DataFrame)
475 def test_market_dataframe_none_when_market_config_absent(self, flex_config_dict):
476 """Test that df_market is None when market config is absent."""
477 # Modify config to remove market config
478 flex_config_dict["market_config"] = None
479 results = Results(
480 flex_config=flex_config_dict,
481 simulator_agent_config=None,
482 generated_flex_files_base_path=SAMPLE_FILES_DIR,
483 results=RESULTS_DIR,
484 )
485 assert results.df_market is None
488# =============================================================================
489# Tests for _load_stats_dataframes
490# =============================================================================
493class TestLoadStatsDataframes:
494 """Tests for the _load_stats_dataframes method."""
496 def test_stats_dataframes_loaded(self, results_instance):
497 """Test that stats DataFrames are loaded."""
498 assert hasattr(results_instance, "df_baseline_stats")
499 assert hasattr(results_instance, "df_pos_flex_stats")
500 assert hasattr(results_instance, "df_neg_flex_stats")
502 def test_stats_dataframes_are_pandas_dataframes(self, results_instance):
503 """Test that stats are pandas DataFrames."""
504 assert isinstance(results_instance.df_baseline_stats, pd.DataFrame)
505 assert isinstance(results_instance.df_pos_flex_stats, pd.DataFrame)
506 assert isinstance(results_instance.df_neg_flex_stats, pd.DataFrame)
509# =============================================================================
510# Tests for convert_timescale_of_dataframe_index
511# =============================================================================
514class TestConvertTimescale:
515 """Tests for the convert_timescale_of_dataframe_index method."""
517 def test_convert_to_minutes(self, flex_config_path, simulator_config_path):
518 """Test converting timescale to minutes."""
519 results = Results(
520 flex_config=flex_config_path,
521 simulator_agent_config=simulator_config_path,
522 generated_flex_files_base_path=SAMPLE_FILES_DIR,
523 results=RESULTS_DIR,
524 to_timescale="seconds",
525 )
526 results.convert_timescale_of_dataframe_index(to_timescale="minutes")
527 assert results.current_timescale_of_data == "minutes"
529 def test_convert_to_hours(self, flex_config_path, simulator_config_path):
530 """Test converting timescale to hours."""
531 results = Results(
532 flex_config=flex_config_path,
533 simulator_agent_config=simulator_config_path,
534 generated_flex_files_base_path=SAMPLE_FILES_DIR,
535 results=RESULTS_DIR,
536 to_timescale="seconds",
537 )
538 results.convert_timescale_of_dataframe_index(to_timescale="hours")
539 assert results.current_timescale_of_data == "hours"
541 def test_convert_updates_current_timescale(self, results_instance):
542 """Test that current_timescale_of_data is updated after conversion."""
543 original_timescale = results_instance.current_timescale_of_data
544 new_timescale = "minutes" if original_timescale != "minutes" else "hours"
545 results_instance.convert_timescale_of_dataframe_index(to_timescale=new_timescale)
546 assert results_instance.current_timescale_of_data == new_timescale
548 def test_convert_multiple_times(self, results_instance):
549 """Test converting timescale multiple times."""
550 results_instance.convert_timescale_of_dataframe_index(to_timescale="minutes")
551 assert results_instance.current_timescale_of_data == "minutes"
553 results_instance.convert_timescale_of_dataframe_index(to_timescale="hours")
554 assert results_instance.current_timescale_of_data == "hours"
556 results_instance.convert_timescale_of_dataframe_index(to_timescale="seconds")
557 assert results_instance.current_timescale_of_data == "seconds"
560# =============================================================================
561# Tests for get_intersection_mpcs_sim
562# =============================================================================
565class TestGetIntersectionMpcsSim:
566 """Tests for the get_intersection_mpcs_sim method."""
568 def test_returns_dict(self, results_instance):
569 """Test that method returns a dictionary."""
570 result = results_instance.get_intersection_mpcs_sim()
571 assert isinstance(result, dict)
573 def test_dict_values_are_dicts(self, results_instance):
574 """Test that dictionary values are also dictionaries."""
575 result = results_instance.get_intersection_mpcs_sim()
576 for key, value in result.items():
577 assert isinstance(value, dict), f"Value for key '{key}' is not a dict"
579 def test_includes_module_ids(self, results_instance):
580 """Test that inner dicts contain module IDs as keys."""
581 result = results_instance.get_intersection_mpcs_sim()
582 if result:
583 first_value = next(iter(result.values()))
584 # Should contain module IDs from the configs
585 assert len(first_value) > 0
588# =============================================================================
589# Tests for create_instance_with_skipped_validation
590# =============================================================================
593class TestCreateInstanceWithSkippedValidation:
594 """Tests for the create_instance_with_skipped_validation method."""
596 def test_bypassed_fields_metadata_stored(self, results_instance):
597 """Test that _bypassed_fields metadata is stored on simulator module config."""
598 if hasattr(results_instance, "simulator_module_config"):
599 assert hasattr(results_instance.simulator_module_config, "_bypassed_fields")
600 assert "result_filename" in results_instance.simulator_module_config._bypassed_fields
602 def test_original_config_metadata_stored(self, results_instance):
603 """Test that _original_config metadata is stored on instance."""
604 if hasattr(results_instance, "simulator_module_config"):
605 assert hasattr(results_instance.simulator_module_config, "_original_config")
606 assert isinstance(results_instance.simulator_module_config._original_config, dict)
608 def test_skipped_field_is_set(self, results_instance):
609 """Test that skipped field (result_filename) is still set on the instance."""
610 if hasattr(results_instance, "simulator_module_config"):
611 assert hasattr(results_instance.simulator_module_config, "result_filename")
614# =============================================================================
615# Tests for __deepcopy__
616# =============================================================================
619class TestDeepCopy:
620 """Tests for the custom __deepcopy__ implementation."""
622 def test_deepcopy_creates_new_instance(self, results_instance):
623 """Test that deepcopy creates a new instance."""
624 copied = copy.deepcopy(results_instance)
625 assert copied is not results_instance
627 def test_deepcopy_preserves_dataframes(self, results_instance):
628 """Test that DataFrames are properly deep copied."""
629 copied = copy.deepcopy(results_instance)
631 # Verify DataFrames exist in copy
632 assert copied.df_baseline is not None
633 assert copied.df_pos_flex is not None
634 assert copied.df_neg_flex is not None
636 # Verify they are different objects
637 assert copied.df_baseline is not results_instance.df_baseline
639 def test_deepcopy_dataframe_independence(self, results_instance):
640 """Test that modifying original doesn't affect copy."""
641 copied = copy.deepcopy(results_instance)
643 # Store original value
644 if len(results_instance.df_baseline) > 0:
645 # [0, 0] is nan due to collocation
646 original_value = copied.df_baseline.iloc[1, 0]
648 # Modify original
649 results_instance.df_baseline.iloc[1, 0] = -999999
651 # Copy should be unchanged
652 assert copied.df_baseline.iloc[1, 0] == original_value
654 def test_deepcopy_preserves_configs(self, results_instance):
655 """Test that configs are preserved in deep copy."""
656 copied = copy.deepcopy(results_instance)
658 assert copied.flex_config is not None
659 assert copied.baseline_agent_config is not None
660 assert copied.baseline_module_config is not None
662 def test_deepcopy_handles_simulator_module_config(self, results_instance):
663 """Test that simulator_module_config with bypassed validation is handled."""
664 copied = copy.deepcopy(results_instance)
666 if hasattr(results_instance, "simulator_module_config"):
667 assert hasattr(copied, "simulator_module_config")
668 assert copied.simulator_module_config is not results_instance.simulator_module_config
669 # Check that bypassed fields metadata is preserved
670 assert hasattr(copied.simulator_module_config, "_bypassed_fields")
672 def test_deepcopy_preserves_timescale(self, results_instance):
673 """Test that current_timescale_of_data is preserved."""
674 results_instance.convert_timescale_of_dataframe_index("minutes")
675 copied = copy.deepcopy(results_instance)
676 assert copied.current_timescale_of_data == "minutes"
679# =============================================================================
680# Tests for _get_flexquant_mpc_module_type
681# =============================================================================
684class TestGetFlexquantMpcModuleType:
685 """Tests for the _get_flexquant_mpc_module_type method."""
687 def test_returns_string(self, results_instance):
688 """Test that method returns a string module type."""
689 module_type = results_instance._get_flexquant_mpc_module_type(
690 results_instance.baseline_agent_config
691 )
692 assert isinstance(module_type, str)
694 def test_raises_when_no_matching_type(self, results_instance):
695 """Test that ModuleNotFoundError is raised when no matching type found."""
696 mock_agent_config = MagicMock()
697 mock_agent_config.id = "test_agent"
698 mock_agent_config.modules = [{"type": "unknown_type"}]
700 with pytest.raises(ModuleNotFoundError, match="no matching mpc module type"):
701 results_instance._get_flexquant_mpc_module_type(mock_agent_config)
704# =============================================================================
705# Tests for agent and module configs loading
706# =============================================================================
709class TestAgentModuleConfigs:
710 """Tests for agent and module configuration loading."""
712 def test_baseline_configs_loaded(self, results_instance):
713 """Test that baseline agent and module configs are loaded."""
714 assert results_instance.baseline_agent_config is not None
715 assert results_instance.baseline_module_config is not None
717 def test_pos_flex_configs_loaded(self, results_instance):
718 """Test that positive flexibility agent and module configs are loaded."""
719 assert results_instance.pos_flex_agent_config is not None
720 assert results_instance.pos_flex_module_config is not None
722 def test_neg_flex_configs_loaded(self, results_instance):
723 """Test that negative flexibility agent and module configs are loaded."""
724 assert results_instance.neg_flex_agent_config is not None
725 assert results_instance.neg_flex_module_config is not None
727 def test_indicator_configs_loaded(self, results_instance):
728 """Test that indicator agent and module configs are loaded."""
729 assert results_instance.indicator_agent_config is not None
730 assert results_instance.indicator_module_config is not None
732 def test_market_configs_loaded_when_present(self, results_instance):
733 """Test that market configs are loaded when market_config is present."""
734 if results_instance.flex_config.market_config:
735 assert results_instance.market_agent_config is not None
736 assert results_instance.market_module_config is not None
739# =============================================================================
740# Integration tests
741# =============================================================================
744class TestResultsIntegration:
745 """Integration tests for the Results class."""
747 def test_full_initialization_workflow(
748 self, flex_config_path, simulator_config_path
749 ):
750 """Test complete initialization with all components."""
751 results = Results(
752 flex_config=flex_config_path,
753 simulator_agent_config=simulator_config_path,
754 generated_flex_files_base_path=SAMPLE_FILES_DIR,
755 results=RESULTS_DIR,
756 to_timescale="seconds",
757 )
759 # Verify all major components are initialized
760 assert results.flex_config is not None
761 assert results.df_baseline is not None
762 assert results.df_pos_flex is not None
763 assert results.df_neg_flex is not None
764 assert results.df_indicator is not None
765 assert results.df_baseline_stats is not None
766 assert results.current_timescale_of_data == "seconds"
768 def test_initialization_without_optional_components(self, flex_config_dict):
769 """Test initialization without market and simulator."""
770 flex_config_dict["market_config"] = None
772 results = Results(
773 flex_config=flex_config_dict,
774 simulator_agent_config=None,
775 generated_flex_files_base_path=SAMPLE_FILES_DIR,
776 results=RESULTS_DIR,
777 to_timescale="seconds",
778 )
780 assert results.df_baseline is not None
781 assert results.df_market is None
783 def test_copy_and_modify_workflow(self, results_instance):
784 """Test copying results and modifying independently."""
785 # Create a copy
786 copied = Results(
787 flex_config=None,
788 simulator_agent_config=None,
789 results=results_instance,
790 )
792 # Modify copy's timescale
793 copied.convert_timescale_of_dataframe_index("hours")
795 # Original should be unchanged
796 assert results_instance.current_timescale_of_data == "seconds"
797 assert copied.current_timescale_of_data == "hours"
799 def test_timescale_conversion_chain(self, results_instance):
800 """Test converting timescale through all options."""
801 # Start with seconds
802 assert results_instance.current_timescale_of_data == "seconds"
804 # Convert to minutes
805 results_instance.convert_timescale_of_dataframe_index("minutes")
806 assert results_instance.current_timescale_of_data == "minutes"
808 # Convert to hours
809 results_instance.convert_timescale_of_dataframe_index("hours")
810 assert results_instance.current_timescale_of_data == "hours"
812 # Convert back to seconds
813 results_instance.convert_timescale_of_dataframe_index("seconds")
814 assert results_instance.current_timescale_of_data == "seconds"
817if __name__ == "__main__":
818 pytest.main([__file__, "-v"])