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
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -164,7 +165,10 @@ def build_epc() -> EpcPropertyData:
heated_rooms_count=3,
door_count=2,
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,
extensions_count=2,
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
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -137,7 +138,10 @@ def build_epc() -> EpcPropertyData:
door_count=2,
percent_draughtproofed=100,
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,
dwelling_type="Mid-Terrace house",
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)
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -180,7 +181,10 @@ def build_epc() -> EpcPropertyData:
door_count=2, # cert lodges 2 doors total
percent_draughtproofed=100,
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,
blocked_chimneys_count=0,
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.
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -190,7 +191,10 @@ def build_epc() -> EpcPropertyData:
door_count=1,
percent_draughtproofed=100,
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,
blocked_chimneys_count=0,
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)
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -146,7 +147,11 @@ def build_epc() -> EpcPropertyData:
heated_rooms_count=4,
door_count=2,
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,
extensions_count=1,
blocked_chimneys_count=0,

View file

@ -21,6 +21,7 @@ Distinct features vs prior fixtures:
as 000480 but on a single-part dwelling
"""
import copy
from typing import Optional
from datatypes.epc.domain.epc_property_data import (
@ -175,7 +176,10 @@ def build_epc() -> EpcPropertyData:
],
percent_draughtproofed=75,
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,
dwelling_type="Mid-Terrace house",
built_form="Mid-Terrace",

View file

@ -2,7 +2,6 @@
from __future__ import annotations
import copy
import dataclasses
import pytest
@ -117,11 +116,8 @@ def test_run_modelling_listed_building_yields_no_wall_insulation() -> None:
def _single_glazed_epc() -> EpcPropertyData:
"""The cavity/floor dwelling with all windows single-glazed — the glazing
generator's trigger, sized so the upgrade reaches the optimised package.
`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())
generator's trigger, sized so the upgrade reaches the optimised package."""
epc: EpcPropertyData = _build_uninsulated_cavity_and_floor_epc()
for window in epc.sap_windows:
window.glazing_type = 1 # SAP10.2 Table U2 code 1 = single.
return epc