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

1""" 

2Tests for the Results class. 

3 

4This test module verifies the Results class functionality for loading and managing 

5flexibility analysis results. 

6 

7Run with: pytest test_results.py -v 

8""" 

9 

10import copy 

11import json 

12import tempfile 

13from pathlib import Path 

14from unittest.mock import MagicMock 

15import sys 

16import pandas as pd 

17import pytest 

18 

19from agentlib_flexquant.data_structures.flex_results import ( 

20 Results, 

21 load_indicator, 

22 load_market, 

23) 

24 

25 

26# ============================================================================= 

27# Path configuration 

28# ============================================================================= 

29 

30SAMPLE_FILES_DIR = Path(__file__).parent / "sample_files" 

31CONFIGS_DIR = SAMPLE_FILES_DIR / "configs" 

32RESULTS_DIR = SAMPLE_FILES_DIR / "sample_results" 

33 

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" 

43 

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" 

54 

55 

56# ============================================================================= 

57# Fixtures 

58# ============================================================================= 

59 

60 

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 

65 

66 

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) 

72 

73 

74@pytest.fixture 

75def simulator_config_path(): 

76 """Return path to simulator config.""" 

77 return SIMULATOR_CONFIG_PATH 

78 

79 

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) 

85 

86 

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 ) 

97 

98 

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 ) 

109 

110 

111@pytest.fixture 

112def temp_dir(): 

113 """Create a temporary directory for test files.""" 

114 with tempfile.TemporaryDirectory() as tmpdir: 

115 yield Path(tmpdir) 

116 

117 

118# ============================================================================= 

119# Tests for standalone loading functions 

120# ============================================================================= 

121 

122 

123class TestLoadIndicator: 

124 """Tests for the load_indicator function.""" 

125 

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) 

130 

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 

136 

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 

141 

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) 

146 

147 

148class TestLoadMarket: 

149 """Tests for the load_market function.""" 

150 

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) 

155 

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 

161 

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 

166 

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) 

171 

172 

173# ============================================================================= 

174# Tests for Results class initialization 

175# ============================================================================= 

176 

177 

178class TestResultsInit: 

179 """Tests for Results class initialization.""" 

180 

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") 

195 

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 

207 

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 

218 

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") 

230 

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 

241 

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 

253 

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 

265 

266 

267# ============================================================================= 

268# Tests for _load_flex_config 

269# ============================================================================= 

270 

271 

272class TestLoadFlexConfig: 

273 """Tests for the _load_flex_config method.""" 

274 

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 

284 

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 

294 

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) 

303 

304 original_base_path = config.get("flex_base_directory_path") 

305 

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 ) 

312 

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 ) 

317 

318 

319# ============================================================================= 

320# Tests for _load_simulator_config 

321# ============================================================================= 

322 

323 

324class TestLoadSimulatorConfig: 

325 """Tests for the _load_simulator_config method.""" 

326 

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 

339 

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 

352 

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") 

364 

365 

366# ============================================================================= 

367# Tests for _resolve_sim_results_path 

368# ============================================================================= 

369 

370 

371class TestResolveSimResultsPath: 

372 """Tests for the _resolve_sim_results_path method.""" 

373 

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() 

380 

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" 

388 

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 ) 

397 

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() 

404 

405 

406# ============================================================================= 

407# Tests for _load_results 

408# ============================================================================= 

409 

410 

411class TestLoadResults: 

412 """Tests for the _load_results method.""" 

413 

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 

422 

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 

433 

434 

435# ============================================================================= 

436# Tests for _load_results_dataframes 

437# ============================================================================= 

438 

439 

440class TestLoadResultsDataframes: 

441 """Tests for the _load_results_dataframes method.""" 

442 

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) 

449 

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 

456 

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 

462 

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") 

468 

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) 

474 

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 

486 

487 

488# ============================================================================= 

489# Tests for _load_stats_dataframes 

490# ============================================================================= 

491 

492 

493class TestLoadStatsDataframes: 

494 """Tests for the _load_stats_dataframes method.""" 

495 

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") 

501 

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) 

507 

508 

509# ============================================================================= 

510# Tests for convert_timescale_of_dataframe_index 

511# ============================================================================= 

512 

513 

514class TestConvertTimescale: 

515 """Tests for the convert_timescale_of_dataframe_index method.""" 

516 

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" 

528 

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" 

540 

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 

547 

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" 

552 

553 results_instance.convert_timescale_of_dataframe_index(to_timescale="hours") 

554 assert results_instance.current_timescale_of_data == "hours" 

555 

556 results_instance.convert_timescale_of_dataframe_index(to_timescale="seconds") 

557 assert results_instance.current_timescale_of_data == "seconds" 

558 

559 

560# ============================================================================= 

561# Tests for get_intersection_mpcs_sim 

562# ============================================================================= 

563 

564 

565class TestGetIntersectionMpcsSim: 

566 """Tests for the get_intersection_mpcs_sim method.""" 

567 

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) 

572 

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" 

578 

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 

586 

587 

588# ============================================================================= 

589# Tests for create_instance_with_skipped_validation 

590# ============================================================================= 

591 

592 

593class TestCreateInstanceWithSkippedValidation: 

594 """Tests for the create_instance_with_skipped_validation method.""" 

595 

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 

601 

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) 

607 

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") 

612 

613 

614# ============================================================================= 

615# Tests for __deepcopy__ 

616# ============================================================================= 

617 

618 

619class TestDeepCopy: 

620 """Tests for the custom __deepcopy__ implementation.""" 

621 

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 

626 

627 def test_deepcopy_preserves_dataframes(self, results_instance): 

628 """Test that DataFrames are properly deep copied.""" 

629 copied = copy.deepcopy(results_instance) 

630 

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 

635 

636 # Verify they are different objects 

637 assert copied.df_baseline is not results_instance.df_baseline 

638 

639 def test_deepcopy_dataframe_independence(self, results_instance): 

640 """Test that modifying original doesn't affect copy.""" 

641 copied = copy.deepcopy(results_instance) 

642 

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] 

647 

648 # Modify original 

649 results_instance.df_baseline.iloc[1, 0] = -999999 

650 

651 # Copy should be unchanged 

652 assert copied.df_baseline.iloc[1, 0] == original_value 

653 

654 def test_deepcopy_preserves_configs(self, results_instance): 

655 """Test that configs are preserved in deep copy.""" 

656 copied = copy.deepcopy(results_instance) 

657 

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 

661 

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) 

665 

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") 

671 

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" 

677 

678 

679# ============================================================================= 

680# Tests for _get_flexquant_mpc_module_type 

681# ============================================================================= 

682 

683 

684class TestGetFlexquantMpcModuleType: 

685 """Tests for the _get_flexquant_mpc_module_type method.""" 

686 

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) 

693 

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"}] 

699 

700 with pytest.raises(ModuleNotFoundError, match="no matching mpc module type"): 

701 results_instance._get_flexquant_mpc_module_type(mock_agent_config) 

702 

703 

704# ============================================================================= 

705# Tests for agent and module configs loading 

706# ============================================================================= 

707 

708 

709class TestAgentModuleConfigs: 

710 """Tests for agent and module configuration loading.""" 

711 

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 

716 

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 

721 

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 

726 

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 

731 

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 

737 

738 

739# ============================================================================= 

740# Integration tests 

741# ============================================================================= 

742 

743 

744class TestResultsIntegration: 

745 """Integration tests for the Results class.""" 

746 

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 ) 

758 

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" 

767 

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 

771 

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 ) 

779 

780 assert results.df_baseline is not None 

781 assert results.df_market is None 

782 

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 ) 

791 

792 # Modify copy's timescale 

793 copied.convert_timescale_of_dataframe_index("hours") 

794 

795 # Original should be unchanged 

796 assert results_instance.current_timescale_of_data == "seconds" 

797 assert copied.current_timescale_of_data == "hours" 

798 

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" 

803 

804 # Convert to minutes 

805 results_instance.convert_timescale_of_dataframe_index("minutes") 

806 assert results_instance.current_timescale_of_data == "minutes" 

807 

808 # Convert to hours 

809 results_instance.convert_timescale_of_dataframe_index("hours") 

810 assert results_instance.current_timescale_of_data == "hours" 

811 

812 # Convert back to seconds 

813 results_instance.convert_timescale_of_dataframe_index("seconds") 

814 assert results_instance.current_timescale_of_data == "seconds" 

815 

816 

817if __name__ == "__main__": 

818 pytest.main([__file__, "-v"])