Coverage for tests/test_SimpleBuilding.py: 93%
57 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-10-20 14:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-10-20 14:09 +0000
1import pytest
2import pandas as pd
3import os
4import sys
5from pathlib import Path
6import importlib.util
7import json
8from util import module_cleanup, round_floats_in_structure
10# Add the project root to the Python path to allow for absolute imports
11# This helps in locating the agentlib_flexquant package if needed
12root_path = Path(__file__).parent.parent
13sys.path.insert(0, str(root_path))
16def create_dataframe_summary(df: pd.DataFrame, precision: int = 6) -> dict:
17 """
18 Creates a robust, compact summary of a DataFrame for snapshotting.
20 This summary is designed to be insensitive to minor floating-point differences
21 while being highly sensitive to meaningful data changes.
23 Args:
24 df: The pandas DataFrame to summarize.
25 precision: The number of decimal places to round float values to.
27 Returns:
28 A dictionary containing the summary.
29 """
30 if df is None or df.empty:
31 return {"error": "DataFrame is empty or None"}
33 # Get descriptive statistics and round them to handle float precision issues
34 summary_stats = df.describe().round(precision)
36 # Convert the stats DataFrame to a dictionary. This may have tuple keys.
37 stats_dict_raw = summary_stats.to_dict()
39 # Create a new dictionary, converting any tuple keys into strings.
40 # e.g., ('lower', 'P_el') becomes 'lower.P_el'
41 stats_dict_clean = {
42 ".".join(map(str, k)) if isinstance(k, tuple) else str(k): v
43 for k, v in stats_dict_raw.items()
44 }
46 # Create the final summary object
47 summary = {
48 "shape": df.shape,
49 "columns": df.columns.tolist(),
50 "index_start": str(df.index.min()),
51 "index_end": str(df.index.max()),
52 "statistics": stats_dict_clean,
53 "head_5_rows": df.head(5).round(precision).to_dict(orient='split'),
54 "tail_5_rows": df.tail(5).round(precision).to_dict(orient='split'),
55 }
56 return summary
58def assert_frame_matches_summary_snapshot(snapshot, df: pd.DataFrame,
59 snapshot_name: str):
60 """
61 Asserts that a DataFrame's summary matches a stored snapshot.
63 This function creates a summary of the dataframe and uses pytest-snapshot
64 to compare it against a stored version.
65 """
66 # Create a summary of the dataframe
67 summary = create_dataframe_summary(df)
69 # Round all numbers in the summary to handle cross-platform differences
70 rounded_summary = round_floats_in_structure(summary, precision=5)
72 # Convert the summary dictionary to a formatted JSON string
73 summary_json = json.dumps(rounded_summary, indent=2, sort_keys=True)
75 # Use snapshot.assert_match on the small, stable JSON string
76 snapshot.assert_match(summary_json, snapshot_name)
78def run_example_from_path(example_path: Path):
79 """
80 Dynamically imports and runs the 'run_example' function from a script
81 in the specified directory.
83 This function robustly handles changing the working directory AND the
84 Python import path, ensuring the script can find both its local files
85 and its local modules.
86 """
87 run_script_path = example_path / 'main_single_run.py'
88 if not run_script_path.is_file():
89 raise FileNotFoundError(
90 f"Could not find the run script at {run_script_path}. "
91 "Please ensure it is named 'run.py' or adjust the test code."
92 )
94 # --- SETUP: Store original paths before changing them ---
95 original_cwd = Path.cwd()
96 original_sys_path = sys.path[:] # Create a copy of the sys.path list
98 module_name = f"agentlib_flexquant.tests.examples.{example_path.name}"
100 try:
101 # --- STEP 1: Change CWD for file access (e.g., config.json) ---
102 os.chdir(example_path)
104 # --- STEP 2: Add example dir to sys.path for module imports ---
105 sys.path.insert(0, str(example_path))
107 # Dynamically import the run_example function from the script
108 spec = importlib.util.spec_from_file_location(module_name, run_script_path)
109 run_module = importlib.util.module_from_spec(spec)
110 sys.modules[module_name] = run_module
111 spec.loader.exec_module(run_module)
113 if not hasattr(run_module, 'run_example'):
114 raise AttributeError(
115 "The 'run.py' script must contain a 'run_example' function.")
117 run_module.sim_config = "mpc_and_sim/fmu_config_linux.json"
119 # Execute the function and get the results
120 results = run_module.run_example(until=3600)
121 return results
123 finally:
124 # --- TEARDOWN: Always restore original paths to avoid side-effects ---
125 os.chdir(original_cwd)
126 sys.path[:] = original_sys_path # Restore the original sys.path
128def test_simplebuilding(snapshot, module_cleanup):
129 """
130 Unit test for the SimpleBuilding example using snapshot testing.
132 This test runs the example via its own run script and compares the
133 full resulting dataframes against stored snapshots.
134 """
135 # Define the path to the example directory
136 example_path = root_path / 'examples' / 'SimpleBuilding'
138 # Run the example and get the results object
139 res = run_example_from_path(example_path)
141 # Extract the full resulting dataframes as requested
142 df_neg_flex_res = res["NegFlexMPC"]["NegFlexMPC"]
143 df_pos_flex_res = res["PosFlexMPC"]["PosFlexMPC"]
144 df_baseline_res = res["FlexModel"]["Baseline"]
145 df_indicator_res = res["FlexibilityIndicator"]["FlexibilityIndicator"]
147 # Assert that a summary of each result DataFrame matches its snapshot
148 assert_frame_matches_summary_snapshot(
149 snapshot,
150 df_neg_flex_res,
151 'SimpleBuilding_neg_flex_summary.json'
152 )
153 assert_frame_matches_summary_snapshot(
154 snapshot,
155 df_pos_flex_res,
156 'SimpleBuilding_pos_flex_summary.json'
157 )
158 assert_frame_matches_summary_snapshot(
159 snapshot,
160 df_baseline_res,
161 'SimpleBuilding_baseline_summary.json'
162 )
163 assert_frame_matches_summary_snapshot(
164 snapshot,
165 df_indicator_res,
166 'SimpleBuilding_indicator_summary.json'
167 )
169if __name__ == "__main__":
170 pytest.main([__file__, "-v"])