Skip to content

Commit e1be239

Browse files
fix(tests): restore forecast_minutes/minutes_now leaked by two tests (#5079)
test_load_forecast_history and test_rate_text_scan both mutate forecast_minutes/minutes_now (and, for the latter, end_record/rate_min/ rate_max/rate_import) on the shared PredBat fixture without restoring them, and each leaked value poisons whichever unrelated test happens to run next depending on shuffle order. load_forecast_history's leak specifically comes from its own trailing restore block calling fetch_config_options(), which re-derives forecast_minutes from apps.yaml (2880) rather than putting back the fixture's overridden value (1440) - a bug in the cleanup itself, not just a missing one. rate_text_scan restored nothing at all. Snapshotting and restoring these fields (following the try/finally pattern already used in test_inverter.py) retires 8 of the 27 known ordering-dependent pairs tracked on #5079 in one structural change per file, rather than patching each victim individually: load_forecast_history -> model_kernel load_forecast_history -> iboost_smart load_forecast_history -> model load_forecast_history -> optimise_windows_kernel rate_text_scan -> cloud_modulation rate_text_scan -> optimise_swap_charge rate_text_scan -> window_sort manual_soc -> cloud_modulation Verified against all 27 known pairs (not just these 8) via ./run_shuffle, and the full ./run_all --quick suite still passes. 19 pairs remain, tracked on #5079. Amended per Copilot review on this PR: test_load_forecast_history's cleanup was still missing several other mutated fixture attributes (now_utc, midnight_utc, load_minutes, load_minutes_age, car_charging_threshold, car_charging_rate, base_load, car_charging_hold, days_previous, days_previous_weight, forecast_days, holiday_days_left, prefix) - audited the whole function (every my_predbat.X assignment now has a matching snapshot+restore) rather than only the fields needed for the 8 pairs above. Doesn't retire any additional known pairs on its own; it closes latent gaps that could surface as a future one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0f22ee1 commit e1be239

2 files changed

Lines changed: 89 additions & 32 deletions

File tree

‎apps/predbat/tests/test_load_forecast_history.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,29 @@ def test_load_forecast_history(my_predbat):
8383
now_utc = datetime(2026, 6, 17, 0, 0, 0, tzinfo=timezone.utc) # Wednesday
8484
original_get_holiday_minutes = my_predbat.get_holiday_minutes
8585
original_args = dict(my_predbat.args) if hasattr(my_predbat, "args") else {}
86+
# setup_predbat() below repeatedly overwrites these for each sub-test, and Test 6 calls
87+
# fetch_config_options() directly, which re-derives forecast_minutes from apps.yaml
88+
# (2880) rather than the fixture's overridden value (1440) - snapshot here so the
89+
# fetch_config_options() call in the restore block below can be put back afterwards,
90+
# instead of leaking the wrong horizon into every later test in the shared fixture (#5079).
91+
original_minutes_now = my_predbat.minutes_now
92+
original_forecast_minutes = my_predbat.forecast_minutes
93+
original_car_charging_energy = my_predbat.car_charging_energy
94+
# setup_predbat() and the sub-tests below also overwrite these; none were being restored
95+
# either (Copilot review on #5097).
96+
original_now_utc = my_predbat.now_utc
97+
original_midnight_utc = my_predbat.midnight_utc
98+
original_load_minutes = my_predbat.load_minutes
99+
original_load_minutes_age = my_predbat.load_minutes_age
100+
original_car_charging_threshold = my_predbat.car_charging_threshold
101+
original_car_charging_rate = my_predbat.car_charging_rate
102+
original_base_load = my_predbat.base_load
103+
original_car_charging_hold = my_predbat.car_charging_hold
104+
original_days_previous = my_predbat.days_previous
105+
original_days_previous_weight = my_predbat.days_previous_weight
106+
original_forecast_days = my_predbat.forecast_days
107+
original_holiday_days_left = my_predbat.holiday_days_left
108+
original_prefix = my_predbat.prefix
86109

87110
# ---------------------------------------------------------------
88111
# Test 1: combined weekday + age weighting end-to-end (holiday neutral)
@@ -521,5 +544,21 @@ def fake_get_history_wrapper(entity_id, days=30, required=True, tracked=True):
521544
my_predbat.args.clear()
522545
my_predbat.args.update(original_args)
523546
my_predbat.fetch_config_options()
547+
my_predbat.minutes_now = original_minutes_now
548+
my_predbat.forecast_minutes = original_forecast_minutes
549+
my_predbat.car_charging_energy = original_car_charging_energy
550+
my_predbat.now_utc = original_now_utc
551+
my_predbat.midnight_utc = original_midnight_utc
552+
my_predbat.load_minutes = original_load_minutes
553+
my_predbat.load_minutes_age = original_load_minutes_age
554+
my_predbat.car_charging_threshold = original_car_charging_threshold
555+
my_predbat.car_charging_rate = original_car_charging_rate
556+
my_predbat.base_load = original_base_load
557+
my_predbat.car_charging_hold = original_car_charging_hold
558+
my_predbat.days_previous = original_days_previous
559+
my_predbat.days_previous_weight = original_days_previous_weight
560+
my_predbat.forecast_days = original_forecast_days
561+
my_predbat.holiday_days_left = original_holiday_days_left
562+
my_predbat.prefix = original_prefix
524563

525564
return failed

‎apps/predbat/tests/test_rate_text_scan.py‎

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,38 +18,56 @@ def test_rate_text_scan(my_predbat):
1818
failed = False
1919
print("**** Testing rate_text_scan ****")
2020

21-
my_predbat.minutes_now = 0
22-
my_predbat.end_record = 9
23-
my_predbat.forecast_minutes = 9
24-
my_predbat.rate_min = 4.0
25-
my_predbat.rate_max = 43.09
26-
27-
# Band A (cheap, minutes 0-2): varies 4.0/10.0/6.0
28-
# Band B (expensive, minutes 3-5): varies 20.0/28.7/22.0
29-
# Band C (very expensive, minutes 6-8): flat 43.1
30-
my_predbat.rate_import = {
31-
0: 4.0,
32-
1: 10.0,
33-
2: 6.0,
34-
3: 20.0,
35-
4: 28.7,
36-
5: 22.0,
37-
6: 43.1,
38-
7: 43.1,
39-
8: 43.1,
40-
}
41-
42-
result = my_predbat.rate_text_scan(export=False)
43-
44-
expected = [
45-
{"start": 0, "end": 3, "rate": "cheap", "range": "(4.0p - 10.0p)"},
46-
{"start": 3, "end": 6, "rate": "expensive", "range": "(20.0p - 28.7p)"},
47-
{"start": 6, "end": 9, "rate": "very expensive", "range": "(43.1p)"},
48-
]
49-
50-
if result != expected:
51-
print(f" ERROR: rate_text_scan mismatch\n got: {result}\n expected: {expected}")
52-
failed = True
21+
# Snapshot every field this test overwrites below and restore it in finally - none of
22+
# this was being restored, so it leaked a shrunk 9-minute horizon and a tiny synthetic
23+
# rate table into every later test sharing this fixture (#5079).
24+
original_minutes_now = my_predbat.minutes_now
25+
original_end_record = my_predbat.end_record
26+
original_forecast_minutes = my_predbat.forecast_minutes
27+
original_rate_min = my_predbat.rate_min
28+
original_rate_max = my_predbat.rate_max
29+
original_rate_import = my_predbat.rate_import
30+
31+
try:
32+
my_predbat.minutes_now = 0
33+
my_predbat.end_record = 9
34+
my_predbat.forecast_minutes = 9
35+
my_predbat.rate_min = 4.0
36+
my_predbat.rate_max = 43.09
37+
38+
# Band A (cheap, minutes 0-2): varies 4.0/10.0/6.0
39+
# Band B (expensive, minutes 3-5): varies 20.0/28.7/22.0
40+
# Band C (very expensive, minutes 6-8): flat 43.1
41+
my_predbat.rate_import = {
42+
0: 4.0,
43+
1: 10.0,
44+
2: 6.0,
45+
3: 20.0,
46+
4: 28.7,
47+
5: 22.0,
48+
6: 43.1,
49+
7: 43.1,
50+
8: 43.1,
51+
}
52+
53+
result = my_predbat.rate_text_scan(export=False)
54+
55+
expected = [
56+
{"start": 0, "end": 3, "rate": "cheap", "range": "(4.0p - 10.0p)"},
57+
{"start": 3, "end": 6, "rate": "expensive", "range": "(20.0p - 28.7p)"},
58+
{"start": 6, "end": 9, "rate": "very expensive", "range": "(43.1p)"},
59+
]
60+
61+
if result != expected:
62+
print(f" ERROR: rate_text_scan mismatch\n got: {result}\n expected: {expected}")
63+
failed = True
64+
finally:
65+
my_predbat.minutes_now = original_minutes_now
66+
my_predbat.end_record = original_end_record
67+
my_predbat.forecast_minutes = original_forecast_minutes
68+
my_predbat.rate_min = original_rate_min
69+
my_predbat.rate_max = original_rate_max
70+
my_predbat.rate_import = original_rate_import
5371

5472
print("**** rate_text_scan tests completed ****")
5573
return failed

0 commit comments

Comments
 (0)