|
| 1 | +# ----------------------------------------------------------------------------- |
| 2 | +# Predbat Home Battery System |
| 3 | +# Copyright Trefor Southwell 2026 - All Rights Reserved |
| 4 | +# This application maybe used for personal use only and not for commercial use |
| 5 | +# ----------------------------------------------------------------------------- |
| 6 | +# fmt off |
| 7 | +# pylint: disable=consider-using-f-string |
| 8 | +# pylint: disable=line-too-long |
| 9 | +# pylint: disable=attribute-defined-outside-init |
| 10 | +"""Tests for how optimise_export copies the export window list it is given. |
| 11 | +
|
| 12 | +optimise_export took a deepcopy of the whole export window list on entry - every window dict, on |
| 13 | +every call - which on the heaviest benchmark scenario was 2.5 million deepcopy calls and the single |
| 14 | +largest block of copying in a plan. |
| 15 | +
|
| 16 | +It does not need one. The trial start is the only thing written, and _prepare_export already applies |
| 17 | +it copy-on-write: it takes its own list and replaces the one window it changes with dict(window, |
| 18 | +start=start). Nothing in optimise_export writes to a window dict at all, so a shallow list copy is |
| 19 | +enough to keep the caller from reordering the list underneath a pending batch. |
| 20 | +
|
| 21 | +Both halves are pinned here: that no deep copy is taken, and that the caller's window dicts still |
| 22 | +come back untouched - the guarantee the deepcopy was there to provide. |
| 23 | +""" |
| 24 | +from tests.test_export_commitment import setup_single_export_window |
| 25 | + |
| 26 | + |
| 27 | +class CountingWindow(dict): |
| 28 | + """An export window that records how many times it has been deep-copied""" |
| 29 | + |
| 30 | + deepcopy_count = 0 |
| 31 | + |
| 32 | + def __deepcopy__(self, memo): |
| 33 | + """Record the copy and return an independent duplicate""" |
| 34 | + CountingWindow.deepcopy_count += 1 |
| 35 | + duplicate = CountingWindow(self) |
| 36 | + memo[id(self)] = duplicate |
| 37 | + return duplicate |
| 38 | + |
| 39 | + |
| 40 | +def run_optimise_export_copy_tests(my_predbat): |
| 41 | + """Run the optimise_export window copying tests""" |
| 42 | + failed = False |
| 43 | + failed |= test_optimise_export_does_not_deepcopy_the_windows(my_predbat) |
| 44 | + failed |= test_optimise_export_leaves_the_callers_windows_untouched(my_predbat) |
| 45 | + return failed |
| 46 | + |
| 47 | + |
| 48 | +def build_windows(my_predbat): |
| 49 | + """Set up a single-export-window scenario whose windows count their own deep copies""" |
| 50 | + export_window_best, record_export_windows, end_record = setup_single_export_window(my_predbat) |
| 51 | + windows = [CountingWindow(window) for window in export_window_best] |
| 52 | + return windows, record_export_windows, end_record |
| 53 | + |
| 54 | + |
| 55 | +def test_optimise_export_does_not_deepcopy_the_windows(my_predbat): |
| 56 | + """The export window list is copied shallowly, so the window dicts are not duplicated""" |
| 57 | + print("**** test_optimise_export_does_not_deepcopy_the_windows ****") |
| 58 | + failed = False |
| 59 | + |
| 60 | + windows, record_export_windows, end_record = build_windows(my_predbat) |
| 61 | + my_predbat.export_window_best = windows |
| 62 | + my_predbat.charge_window_best = [] |
| 63 | + my_predbat.charge_limit_best = [] |
| 64 | + |
| 65 | + CountingWindow.deepcopy_count = 0 |
| 66 | + my_predbat.optimise_export(0, record_export_windows, [], [], windows, [0.0], end_record=end_record) |
| 67 | + |
| 68 | + if CountingWindow.deepcopy_count != 0: |
| 69 | + print("ERROR: optimise_export deep-copied the export windows {} times".format(CountingWindow.deepcopy_count)) |
| 70 | + failed = True |
| 71 | + |
| 72 | + my_predbat.export_window_best = [] |
| 73 | + if not failed: |
| 74 | + print("PASS") |
| 75 | + return failed |
| 76 | + |
| 77 | + |
| 78 | +def test_optimise_export_leaves_the_callers_windows_untouched(my_predbat): |
| 79 | + """The caller's window dicts must come back exactly as they went in. |
| 80 | +
|
| 81 | + This is what the deepcopy was defending. _prepare_export now applies the trial start to its own |
| 82 | + copy instead, so if that ever reverts to an in-place write this fails rather than silently |
| 83 | + corrupting every other trial sharing the list. |
| 84 | + """ |
| 85 | + print("**** test_optimise_export_leaves_the_callers_windows_untouched ****") |
| 86 | + failed = False |
| 87 | + |
| 88 | + windows, record_export_windows, end_record = build_windows(my_predbat) |
| 89 | + my_predbat.export_window_best = windows |
| 90 | + my_predbat.charge_window_best = [] |
| 91 | + my_predbat.charge_limit_best = [] |
| 92 | + |
| 93 | + before = [dict(window) for window in windows] |
| 94 | + identities = [id(window) for window in windows] |
| 95 | + |
| 96 | + my_predbat.optimise_export(0, record_export_windows, [], [], windows, [0.0], end_record=end_record) |
| 97 | + |
| 98 | + after = [dict(window) for window in windows] |
| 99 | + if after != before: |
| 100 | + print("ERROR: optimise_export modified the caller's windows") |
| 101 | + print(" before {}".format(before)) |
| 102 | + print(" after {}".format(after)) |
| 103 | + failed = True |
| 104 | + if [id(window) for window in windows] != identities: |
| 105 | + print("ERROR: optimise_export replaced entries in the caller's window list") |
| 106 | + failed = True |
| 107 | + |
| 108 | + my_predbat.export_window_best = [] |
| 109 | + if not failed: |
| 110 | + print("PASS") |
| 111 | + return failed |
0 commit comments