test(fixtures): build_epc() deep-copies its windows so callers can't leak state

The worksheet build_epc() fixtures wrapped a module-level SECTION_6_VERTICAL_
WINDOWS tuple in list(), so every call returned the SAME SapWindow objects. A
test that mutated a returned window (the glazing slices flip glazing_type to
single) leaked that change into every later build_epc() -- which surfaced as
double_glazing-product failures in the first-run integration tests only when
test_console ran first in the same process.

Deep-copy the windows per call in all six fixtures (000474/477/480/487/490/516)
so each EpcPropertyData owns an independent window graph, and drop the
now-redundant defensive copy at the glazing test's call site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-05 09:36:56 +00:00
parent 456a81df0a
commit 4783ff9dfd
7 changed files with 33 additions and 12 deletions

View file

@ -19,6 +19,7 @@ Distinct features vs prior fixtures:
first 5.27) the upper storey is smaller than the ground first 5.27) the upper storey is smaller than the ground
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -164,7 +165,10 @@ def build_epc() -> EpcPropertyData:
heated_rooms_count=3, heated_rooms_count=3,
door_count=2, door_count=2,
low_energy_fixed_lighting_bulbs_count=8, low_energy_fixed_lighting_bulbs_count=8,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows (the module-level
# SECTION_6_VERTICAL_WINDOWS tuple holds shared SapWindow objects; a
# caller mutating a returned window would otherwise leak across calls).
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
percent_draughtproofed=78, percent_draughtproofed=78,
extensions_count=2, extensions_count=2,
blocked_chimneys_count=0, blocked_chimneys_count=0,

View file

@ -15,6 +15,7 @@ Distinct features vs prior fixtures:
flat ceiling just stud walls (1.5/1.3 height) + slopes flat ceiling just stud walls (1.5/1.3 height) + slopes
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -137,7 +138,10 @@ def build_epc() -> EpcPropertyData:
door_count=2, door_count=2,
percent_draughtproofed=100, percent_draughtproofed=100,
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL, low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows (the module-level
# SECTION_6_VERTICAL_WINDOWS tuple holds shared SapWindow objects; a
# caller mutating a returned window would otherwise leak across calls).
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
blocked_chimneys_count=0, blocked_chimneys_count=0,
dwelling_type="Mid-Terrace house", dwelling_type="Mid-Terrace house",
built_form="Mid-Terrace", built_form="Mid-Terrace",

View file

@ -16,6 +16,7 @@ Differs from 000487 along several useful axes:
- No alternative wall (vs 1 timber-frame alt wall on the extension) - No alternative wall (vs 1 timber-frame alt wall on the extension)
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -180,7 +181,10 @@ def build_epc() -> EpcPropertyData:
door_count=2, # cert lodges 2 doors total door_count=2, # cert lodges 2 doors total
percent_draughtproofed=100, percent_draughtproofed=100,
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL, low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows (the module-level
# SECTION_6_VERTICAL_WINDOWS tuple holds shared SapWindow objects; a
# caller mutating a returned window would otherwise leak across calls).
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
extensions_count=1, extensions_count=1,
blocked_chimneys_count=0, blocked_chimneys_count=0,
dwelling_type="Mid-Terrace house", dwelling_type="Mid-Terrace house",

View file

@ -13,6 +13,7 @@ values captured below. Treat the LINE_X constants as authoritative; if
they diverge from our code, the bug is on our side. they diverge from our code, the bug is on our side.
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -190,7 +191,10 @@ def build_epc() -> EpcPropertyData:
door_count=1, door_count=1,
percent_draughtproofed=100, percent_draughtproofed=100,
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL, low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows (the module-level
# SECTION_6_VERTICAL_WINDOWS tuple holds shared SapWindow objects; a
# caller mutating a returned window would otherwise leak across calls).
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
extensions_count=1, extensions_count=1,
blocked_chimneys_count=0, blocked_chimneys_count=0,
dwelling_type="Enclosed Mid-Terrace house", dwelling_type="Enclosed Mid-Terrace house",

View file

@ -21,6 +21,7 @@ Distinct features vs prior fixtures:
- DP = 100%, so (15) = 0.05 (lowest window-infiltration component) - DP = 100%, so (15) = 0.05 (lowest window-infiltration component)
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -146,7 +147,11 @@ def build_epc() -> EpcPropertyData:
heated_rooms_count=4, heated_rooms_count=4,
door_count=2, door_count=2,
low_energy_fixed_lighting_bulbs_count=8, low_energy_fixed_lighting_bulbs_count=8,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows: SECTION_6_VERTICAL_
# WINDOWS is a module-level tuple of shared SapWindow objects, and a
# caller that mutates a returned window (e.g. flipping glazing_type to
# test a glazing measure) would otherwise leak into every later call.
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
percent_draughtproofed=100, percent_draughtproofed=100,
extensions_count=1, extensions_count=1,
blocked_chimneys_count=0, blocked_chimneys_count=0,

View file

@ -21,6 +21,7 @@ Distinct features vs prior fixtures:
as 000480 but on a single-part dwelling as 000480 but on a single-part dwelling
""" """
import copy
from typing import Optional from typing import Optional
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
@ -175,7 +176,10 @@ def build_epc() -> EpcPropertyData:
], ],
percent_draughtproofed=75, percent_draughtproofed=75,
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL, low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS), # Deep-copy so each build_epc() owns its windows (the module-level
# SECTION_6_VERTICAL_WINDOWS tuple holds shared SapWindow objects; a
# caller mutating a returned window would otherwise leak across calls).
sap_windows=[copy.deepcopy(window) for window in SECTION_6_VERTICAL_WINDOWS],
blocked_chimneys_count=0, blocked_chimneys_count=0,
dwelling_type="Mid-Terrace house", dwelling_type="Mid-Terrace house",
built_form="Mid-Terrace", built_form="Mid-Terrace",

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import copy
import dataclasses import dataclasses
import pytest import pytest
@ -117,11 +116,8 @@ def test_run_modelling_listed_building_yields_no_wall_insulation() -> None:
def _single_glazed_epc() -> EpcPropertyData: def _single_glazed_epc() -> EpcPropertyData:
"""The cavity/floor dwelling with all windows single-glazed — the glazing """The cavity/floor dwelling with all windows single-glazed — the glazing
generator's trigger, sized so the upgrade reaches the optimised package. generator's trigger, sized so the upgrade reaches the optimised package."""
epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc()
`build_epc()` shares its window objects across calls, so deep-copy before
mutating to avoid leaking single-glazing into other tests' baselines."""
epc: EpcPropertyData = copy.deepcopy(_build_uninsulated_cavity_and_floor_epc())
for window in epc.sap_windows: for window in epc.sap_windows:
window.glazing_type = 1 # SAP10.2 Table U2 code 1 = single. window.glazing_type = 1 # SAP10.2 Table U2 code 1 = single.
return epc return epc