WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Host tests for `vibe.pa_sketch_extents` -- the two drawing functions
|
||||
(`outline()` / `section()`) the Concrete Extents page's new `drawing` blocks
|
||||
name via `module: 'pa_sketch_extents'` (see `pa_extents.py`'s "WHY THE TWO
|
||||
DRAWINGS ARE NOW STACKED" docstring note for the 2026-09-04 owner ruling this
|
||||
exists to satisfy). Loaded the way `test_panelsketch.py` is -- `conftest.py`
|
||||
registers a bare `vibe` package pointed at `lib/vibe` so this imports on
|
||||
plain host CPython with no Revit and no `pyrevit` on the path.
|
||||
|
||||
What this suite checks, per the task brief: `outline()` never returns an
|
||||
empty/blank canvas even when geometry is missing; the cut-line tick + label
|
||||
move with `values['_cut_frac']`; every primitive from both functions stays
|
||||
inside the canvas box; `section()` removes an opening's gap only when the
|
||||
cut station falls inside that opening's along-length range; a Reveal cuts a
|
||||
partial-depth notch instead; a Solid Zone removes nothing; a Sandwich
|
||||
section draws its wythe/insulation layers; the printed net area matches the
|
||||
gross area minus what was actually drawn as removed; and neither function
|
||||
raises or mutates `values`.
|
||||
"""
|
||||
import copy
|
||||
|
||||
from vibe import panelsketch
|
||||
from vibe import pa_sketch_extents as S
|
||||
|
||||
|
||||
# --------------------------------------------------------------- utilities
|
||||
|
||||
def _values(**over):
|
||||
base = {'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
|
||||
'mark': 'PNL101', 'design_number': 'PNL1'}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
def _rows(primitives, kind, role=None):
|
||||
return [p for p in primitives
|
||||
if p['kind'] == kind and (role is None or p.get('role') == role)]
|
||||
|
||||
|
||||
def _within_box(primitives, width_px, height_px, tol=1e-6):
|
||||
for p in primitives:
|
||||
if p['kind'] == 'rect':
|
||||
assert -tol <= p['x'] <= width_px + tol, p
|
||||
assert -tol <= p['y'] <= height_px + tol, p
|
||||
assert -tol <= p['x'] + p['w'] <= width_px + tol, p
|
||||
assert -tol <= p['y'] + p['h'] <= height_px + tol, p
|
||||
elif p['kind'] == 'line':
|
||||
for kx, ky in (('x1', 'y1'), ('x2', 'y2')):
|
||||
assert -tol <= p[kx] <= width_px + tol, p
|
||||
assert -tol <= p[ky] <= height_px + tol, p
|
||||
else:
|
||||
if 'x' in p:
|
||||
assert -tol <= p['x'] <= width_px + tol, p
|
||||
if 'y' in p:
|
||||
assert -tol <= p['y'] <= height_px + tol, p
|
||||
|
||||
|
||||
def _tick_x(primitives):
|
||||
ticks = _rows(primitives, 'line', 'cutline-tick')
|
||||
assert len(ticks) == 2
|
||||
xs = set()
|
||||
for t in ticks:
|
||||
assert abs(t['x1'] - t['x2']) < 1e-9 # a tick is vertical
|
||||
xs.add(round(t['x1'], 6))
|
||||
assert len(xs) == 1, 'top and bottom ticks should share one x'
|
||||
return ticks[0]['x1']
|
||||
|
||||
|
||||
_OPENING_ROW = {'type': 'Opening', 'pos_along': 10, 'pos_along_from': 'start',
|
||||
'length': 4, 'pos_vert': 2, 'height': 3, 'label': 'DOOR'}
|
||||
|
||||
|
||||
# =========================================================== outline() ===
|
||||
|
||||
def test_outline_missing_geometry_is_a_note_not_a_blank_canvas():
|
||||
primitives, problems = S.outline(_values(length_ft=''), 400, 300)
|
||||
assert primitives, 'the canvas must never come back empty'
|
||||
assert any(p['kind'] == 'text' for p in primitives)
|
||||
assert problems
|
||||
|
||||
|
||||
def test_outline_draws_the_panel_and_readable_overall_labels():
|
||||
primitives, _problems = S.outline(
|
||||
_values(length_ft='40', width_ft='8'), 800, 300)
|
||||
rects = _rows(primitives, 'rect', 'panel')
|
||||
assert len(rects) == 1
|
||||
texts = _rows(primitives, 'text', 'dimension')
|
||||
l_text = [t for t in texts if t['text'].startswith('L = ')]
|
||||
w_text = [t for t in texts if t['text'].startswith('W = ')]
|
||||
assert l_text and l_text[0]['font_size'] >= 11
|
||||
assert w_text and w_text[0]['font_size'] >= 11
|
||||
|
||||
|
||||
def test_outline_draws_cutline_tick_and_station_label():
|
||||
primitives, _problems = S.outline(
|
||||
_values(length_ft='40', width_ft='8', _cut_frac='0.25'), 800, 300)
|
||||
labels = _rows(primitives, 'text', 'cutline-label')
|
||||
assert len(labels) == 1
|
||||
assert labels[0]['text'] == 'cut @ 10.0 ft'
|
||||
assert labels[0]['font_size'] >= 11
|
||||
|
||||
|
||||
def test_outline_cut_tick_moves_with_cut_frac():
|
||||
values_lo = _values(length_ft='40', width_ft='8', _cut_frac='0.2')
|
||||
values_hi = _values(length_ft='40', width_ft='8', _cut_frac='0.8')
|
||||
prims_lo, _ = S.outline(values_lo, 800, 300)
|
||||
prims_hi, _ = S.outline(values_hi, 800, 300)
|
||||
x_lo = _tick_x(prims_lo)
|
||||
x_hi = _tick_x(prims_hi)
|
||||
assert x_hi > x_lo
|
||||
|
||||
|
||||
def test_outline_cut_frac_defaults_to_midpoint():
|
||||
primitives, _problems = S.outline(
|
||||
_values(length_ft='40', width_ft='8'), 800, 300)
|
||||
labels = _rows(primitives, 'text', 'cutline-label')
|
||||
assert labels[0]['text'] == 'cut @ 20.0 ft'
|
||||
|
||||
|
||||
def test_outline_cut_frac_out_of_range_clamps():
|
||||
lo_prims, _ = S.outline(
|
||||
_values(length_ft='40', width_ft='8', _cut_frac='-3'), 800, 300)
|
||||
hi_prims, _ = S.outline(
|
||||
_values(length_ft='40', width_ft='8', _cut_frac='9'), 800, 300)
|
||||
assert _rows(lo_prims, 'text', 'cutline-label')[0]['text'] == 'cut @ 0.0 ft'
|
||||
assert _rows(hi_prims, 'text', 'cutline-label')[0]['text'] == 'cut @ 40.0 ft'
|
||||
|
||||
|
||||
def test_outline_draws_openings_to_scale():
|
||||
values = _values(length_ft='40', width_ft='10',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
primitives, _problems = S.outline(values, 800, 300)
|
||||
assert len(_rows(primitives, 'rect', 'opening')) == 1
|
||||
|
||||
|
||||
def test_outline_stays_inside_box():
|
||||
values = _values(length_ft='40', width_ft='10',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
for w, h in ((800, 300), (60, 60), (20, 15)):
|
||||
primitives, _problems = S.outline(values, w, h)
|
||||
_within_box(primitives, w, h)
|
||||
|
||||
|
||||
def test_outline_never_mutates_values():
|
||||
values = _values(length_ft='40', width_ft='10', _cut_frac='0.3',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
before = copy.deepcopy(values)
|
||||
S.outline(values, 800, 300)
|
||||
assert values == before
|
||||
|
||||
|
||||
def test_outline_never_raises_on_garbage():
|
||||
garbage = {'length_ft': object(), 'width_ft': [1, 2], '_cut_frac': {},
|
||||
'openings_rows': 'not-a-list'}
|
||||
primitives, problems = S.outline(garbage, 800, 300)
|
||||
assert isinstance(primitives, list) and primitives
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
def test_outline_degrades_when_panelsketch_unavailable(monkeypatch):
|
||||
# `outline()` no longer needs `panelsketch` for the panel rectangle, the
|
||||
# L/W labels or the cut marker (see the module docstring's "WHY
|
||||
# `outline()` NO LONGER DELEGATES" note) -- only openings/reveals/solid
|
||||
# zones and the title are lost. So the picture still draws; only the
|
||||
# openings problem is what names `panelsketch` as missing.
|
||||
monkeypatch.setattr(S, 'panelsketch', None)
|
||||
primitives, problems = S.outline(
|
||||
_values(length_ft='40', width_ft='8'), 800, 300)
|
||||
assert _rows(primitives, 'rect', 'panel')
|
||||
assert any('panelsketch' in p for p in problems)
|
||||
|
||||
|
||||
def test_outline_degrades_when_openings_row_parsing_raises(monkeypatch):
|
||||
# The panel rectangle and its labels must survive even if the
|
||||
# `panelsketch` openings machinery this module reuses blows up --
|
||||
# never take the whole outline down for a row-parsing bug.
|
||||
def _boom(values, length_ft, width_ft, thickness_ft):
|
||||
raise RuntimeError('boom')
|
||||
monkeypatch.setattr(panelsketch, '_openings_rows', _boom)
|
||||
values = _values(length_ft='40', width_ft='8',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
primitives, problems = S.outline(values, 800, 300)
|
||||
assert _rows(primitives, 'rect', 'panel')
|
||||
assert any('boom' in p for p in problems)
|
||||
|
||||
|
||||
def test_outline_panel_spans_at_least_85pct_of_canvas_width():
|
||||
# The headless-render finding this rewrite exists to fix: the panel
|
||||
# rectangle must fill (almost) the whole canvas width, never letterboxed
|
||||
# down to roughly half of it. Uses the real target dimensions from the
|
||||
# owner's render (37.58 x 8.375 ft x 8 in) at a realistic canvas size.
|
||||
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
|
||||
primitives, _problems = S.outline(values, 1100, 300)
|
||||
panel = _rows(primitives, 'rect', 'panel')[0]
|
||||
assert panel['w'] >= 0.85 * 1100
|
||||
|
||||
|
||||
def test_outline_cut_and_l_labels_stay_off_the_cut_tick():
|
||||
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
|
||||
primitives, _problems = S.outline(values, 1100, 300)
|
||||
tick_x = _tick_x(primitives)
|
||||
cut_label = _rows(primitives, 'text', 'cutline-label')[0]
|
||||
l_label = [t for t in _rows(primitives, 'text', 'dimension')
|
||||
if t['text'].startswith('L = ')][0]
|
||||
assert abs(cut_label['x'] - tick_x) > 12.0
|
||||
assert abs(l_label['x'] - tick_x) > 12.0
|
||||
|
||||
|
||||
def test_outline_opening_rect_scales_with_length():
|
||||
row = dict(_OPENING_ROW) # pos_along=10, length=4, from 'start'
|
||||
short = S.outline(_values(length_ft='20', width_ft='10',
|
||||
openings_rows=[row]), 1000, 300)[0]
|
||||
long_ = S.outline(_values(length_ft='40', width_ft='10',
|
||||
openings_rows=[row]), 1000, 300)[0]
|
||||
w_short = _rows(short, 'rect', 'opening')[0]['w']
|
||||
w_long = _rows(long_, 'rect', 'opening')[0]['w']
|
||||
# the same absolute 4 ft opening is a bigger FRACTION of a 20 ft panel
|
||||
# than of a 40 ft one, so it must draw roughly half as wide in pixels.
|
||||
assert w_long < w_short
|
||||
assert abs(w_long - w_short / 2.0) < 1.0
|
||||
|
||||
|
||||
def test_outline_geometry_matches_the_drawn_panel_rect():
|
||||
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
|
||||
for w, h in ((1100, 300), (800, 260), (600, 220)):
|
||||
primitives, _problems = S.outline(values, w, h)
|
||||
panel = _rows(primitives, 'rect', 'panel')[0]
|
||||
geo = S.outline_geometry(values, w, h)
|
||||
assert abs(panel['x'] - geo['x0']) < 1e-6
|
||||
assert abs(panel['y'] - geo['y0']) < 1e-6
|
||||
assert abs(panel['x'] + panel['w'] - geo['x1']) < 1e-6
|
||||
assert abs(panel['y'] + panel['h'] - geo['y1']) < 1e-6
|
||||
|
||||
|
||||
def test_outline_geometry_cut_tick_matches_the_geometry_helper():
|
||||
values = _values(length_ft='37.58', width_ft='8.375', _cut_frac='0.3')
|
||||
primitives, _problems = S.outline(values, 1100, 300)
|
||||
tick_x = _tick_x(primitives)
|
||||
geo = S.outline_geometry(values, 1100, 300)
|
||||
expected = geo['x0'] + 0.3 * (geo['x1'] - geo['x0'])
|
||||
assert abs(tick_x - expected) < 1e-6
|
||||
|
||||
|
||||
def _all_ys(primitives):
|
||||
ys = []
|
||||
for p in primitives:
|
||||
if p['kind'] == 'rect':
|
||||
ys.append(p['y'])
|
||||
ys.append(p['y'] + p['h'])
|
||||
elif p['kind'] == 'line':
|
||||
ys.append(p['y1'])
|
||||
ys.append(p['y2'])
|
||||
elif 'y' in p:
|
||||
ys.append(p['y'])
|
||||
return ys
|
||||
|
||||
|
||||
def test_outline_every_primitive_has_4px_clearance_top_and_bottom():
|
||||
# Live-render finding, 2026-09-04: the panel's bottom edge was pinned to
|
||||
# the canvas bottom and the L label clipped off entirely -- both because
|
||||
# nothing reserved room outside the panel rectangle. Checked at several
|
||||
# realistic canvas sizes (the container's real height is
|
||||
# `width_px * aspect`, `aspect` from `pa_extents` -- see
|
||||
# `_OUTLINE_LABEL_ROWS_ASPECT`), not the degenerate tiny-canvas sizes
|
||||
# `test_outline_stays_inside_box` exercises (where 4px clearance is not
|
||||
# geometrically possible at all).
|
||||
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
for width_px in (600, 900, 1100):
|
||||
height_px = width_px * 0.4232 # width_ft/length_ft (~0.2228,
|
||||
# clamped through [0.12, 0.5]) + 0.10
|
||||
primitives, _problems = S.outline(values, width_px, height_px)
|
||||
for y in _all_ys(primitives):
|
||||
assert -1e-6 <= y <= height_px + 1e-6, (y, height_px)
|
||||
assert y >= 4.0 - 1e-6, 'too close to the top: {0}'.format(y)
|
||||
assert y <= height_px - 4.0 + 1e-6, \
|
||||
'too close to the bottom: {0}'.format(y)
|
||||
|
||||
|
||||
def test_outline_l_label_sits_below_the_panel_rect():
|
||||
values = _values(length_ft='37.58', width_ft='8.375', thickness_in='8')
|
||||
primitives, _problems = S.outline(values, 1100, 1100 * 0.4232)
|
||||
panel = _rows(primitives, 'rect', 'panel')[0]
|
||||
l_label = [t for t in _rows(primitives, 'text', 'dimension')
|
||||
if t['text'].startswith('L = ')][0]
|
||||
assert l_label['y'] > panel['y'] + panel['h']
|
||||
|
||||
|
||||
def test_outline_geometry_never_raises_and_always_returns_a_box():
|
||||
geo = S.outline_geometry(
|
||||
{'length_ft': object(), 'width_ft': None}, 'nope', [])
|
||||
for key in ('x0', 'x1', 'y0', 'y1'):
|
||||
assert key in geo
|
||||
assert geo['x1'] >= geo['x0']
|
||||
assert geo['y1'] >= geo['y0']
|
||||
|
||||
|
||||
# =========================================================== section() ===
|
||||
|
||||
def test_section_missing_width_or_thickness_is_a_note_not_a_blank_canvas():
|
||||
primitives, problems = S.section(
|
||||
_values(width_ft='', thickness_in=''), 400, 300)
|
||||
assert primitives and primitives[0]['kind'] == 'text'
|
||||
assert any('Width' in p for p in problems)
|
||||
assert any('Thickness' in p for p in problems)
|
||||
|
||||
|
||||
def test_section_draws_the_gross_rectangle_and_labels():
|
||||
primitives, _problems = S.section(
|
||||
_values(width_ft='8.375', thickness_in='8', length_ft='24'),
|
||||
600, 260)
|
||||
rects = _rows(primitives, 'rect', 'panel-section')
|
||||
assert len(rects) == 1
|
||||
t_text = [t['text'] for t in _rows(primitives, 'text', 'dimension')
|
||||
if t['text'].startswith('t = ')]
|
||||
w_text = [t['text'] for t in _rows(primitives, 'text', 'dimension')
|
||||
if t['text'].startswith('W = ')]
|
||||
assert t_text == ['t = 8.00 in']
|
||||
assert w_text == ['W = 8.38 ft']
|
||||
|
||||
|
||||
def test_section_net_area_equals_gross_when_no_openings():
|
||||
primitives, _problems = S.section(
|
||||
_values(width_ft='8', thickness_in='8', length_ft='24',
|
||||
openings_rows=[]),
|
||||
600, 260)
|
||||
net = _rows(primitives, 'text', 'annotation')
|
||||
gross_in2 = 8.0 * 12.0 * 8.0
|
||||
assert len(net) == 1
|
||||
assert '{0:.1f}'.format(gross_in2) in net[0]['text']
|
||||
|
||||
|
||||
def test_section_opening_inside_along_range_removes_a_gap_and_area():
|
||||
# station = 0.5 * 24 = 12 ft, inside the opening's [10, 14] along range.
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.5',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
primitives, _problems = S.section(values, 600, 260)
|
||||
gaps = _rows(primitives, 'rect', 'opening')
|
||||
assert len(gaps) == 1
|
||||
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
|
||||
gross_in2 = 10.0 * 12.0 * 8.0
|
||||
removed_in2 = 3.0 * 12.0 * 8.0 # pos_vert=2..height=3 -> [2,5] * full t
|
||||
expected_net = gross_in2 - removed_in2
|
||||
assert '{0:.1f}'.format(expected_net) in net_text
|
||||
|
||||
|
||||
def test_section_opening_outside_along_range_draws_nothing():
|
||||
# station = 0.05 * 24 = 1.2 ft, outside the opening's [10, 14] range.
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.05',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
primitives, _problems = S.section(values, 600, 260)
|
||||
assert not _rows(primitives, 'rect', 'opening')
|
||||
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
|
||||
gross_in2 = 10.0 * 12.0 * 8.0
|
||||
assert '{0:.1f}'.format(gross_in2) in net_text
|
||||
|
||||
|
||||
def test_section_reveal_cuts_a_partial_depth_notch():
|
||||
# width=2/12 ft (2 in) -- a reveal deeper than the 8 in thickness would
|
||||
# be nonsensical, and internal units are decimal feet throughout this
|
||||
# codebase (`CLAUDE.md` sec.3), the same as `pos_along` / `length` above.
|
||||
row = {'type': 'Reveal', 'pos_along': 10, 'pos_along_from': 'start',
|
||||
'length': 4, 'pos_vert': 2, 'height': 3, 'pos_cross': 0,
|
||||
'pos_cross_from': 'left', 'width': 2.0 / 12.0, 'label': 'REVEAL1'}
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.5', openings_rows=[row])
|
||||
primitives, _problems = S.section(values, 600, 260)
|
||||
reveals = _rows(primitives, 'rect', 'reveal')
|
||||
assert len(reveals) == 1
|
||||
panel = _rows(primitives, 'rect', 'panel-section')[0]
|
||||
# a reveal 2 in deep out of an 8 in thickness must be shorter than the
|
||||
# full panel-section height, and still sit inside it.
|
||||
assert reveals[0]['h'] < panel['h']
|
||||
assert reveals[0]['y'] >= panel['y'] - 1e-6
|
||||
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
|
||||
gross_in2 = 10.0 * 12.0 * 8.0
|
||||
removed_in2 = 3.0 * 12.0 * 2.0 # vert extent (ft->in) * cross depth, in
|
||||
expected_net = gross_in2 - removed_in2
|
||||
assert '{0:.1f}'.format(expected_net) in net_text
|
||||
|
||||
|
||||
def test_section_solid_zone_at_station_removes_nothing():
|
||||
row = {'type': 'Solid Zone', 'pos_along': 10, 'pos_along_from': 'start',
|
||||
'length': 4, 'pos_vert': 2, 'height': 3, 'label': 'SZ1'}
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.5', openings_rows=[row])
|
||||
primitives, problems = S.section(values, 600, 260)
|
||||
assert not _rows(primitives, 'rect', 'opening')
|
||||
assert not _rows(primitives, 'rect', 'reveal')
|
||||
net_text = _rows(primitives, 'text', 'annotation')[0]['text']
|
||||
gross_in2 = 10.0 * 12.0 * 8.0
|
||||
assert '{0:.1f}'.format(gross_in2) in net_text
|
||||
assert any('Solid Zone' in p for p in problems)
|
||||
|
||||
|
||||
def test_section_length_missing_draws_no_openings_but_reports_why():
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='',
|
||||
openings_rows=[dict(_OPENING_ROW)])
|
||||
primitives, problems = S.section(values, 600, 260)
|
||||
assert not _rows(primitives, 'rect', 'opening')
|
||||
assert any('Length' in p for p in problems)
|
||||
station = _rows(primitives, 'text', 'cutline-label')
|
||||
assert station and 'not entered' in station[0]['text']
|
||||
|
||||
|
||||
def test_section_sandwich_draws_wythe_and_insulation_layers():
|
||||
values = _values(width_ft='8', thickness_in='8', length_ft='24',
|
||||
section_type='Sandwich', wythe_top_in='2',
|
||||
insulation_in='2', wythe_bottom_in='4')
|
||||
primitives, problems = S.section(values, 600, 260)
|
||||
assert len(_rows(primitives, 'rect', 'wythe')) == 2
|
||||
assert len(_rows(primitives, 'rect', 'insulation')) == 1
|
||||
assert not any('do not add up' in p for p in problems)
|
||||
|
||||
|
||||
def test_section_sandwich_missing_layers_falls_back_to_one_outline():
|
||||
values = _values(width_ft='8', thickness_in='8', length_ft='24',
|
||||
section_type='Sandwich')
|
||||
primitives, problems = S.section(values, 600, 260)
|
||||
assert not _rows(primitives, 'rect', 'wythe')
|
||||
assert any('not all entered' in p for p in problems)
|
||||
|
||||
|
||||
def test_section_openings_rows_as_plain_text_is_reported_not_guessed():
|
||||
values = _values(width_ft='8', thickness_in='8', length_ft='24',
|
||||
openings='2 openings near the middle')
|
||||
primitives, problems = S.section(values, 600, 260)
|
||||
assert not _rows(primitives, 'rect', 'opening')
|
||||
assert any('plain text' in p for p in problems)
|
||||
|
||||
|
||||
def test_section_stays_inside_box():
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.5', openings_rows=[dict(_OPENING_ROW)])
|
||||
for w, h in ((600, 260), (60, 60), (20, 15)):
|
||||
primitives, _problems = S.section(values, w, h)
|
||||
_within_box(primitives, w, h)
|
||||
|
||||
|
||||
def test_section_never_mutates_values():
|
||||
values = _values(width_ft='10', thickness_in='8', length_ft='24',
|
||||
_cut_frac='0.5', openings_rows=[dict(_OPENING_ROW)])
|
||||
before = copy.deepcopy(values)
|
||||
S.section(values, 600, 260)
|
||||
assert values == before
|
||||
|
||||
|
||||
def test_section_never_raises_on_garbage():
|
||||
garbage = {'width_ft': object(), 'thickness_in': [1], 'length_ft': {},
|
||||
'_cut_frac': 'nope', 'openings_rows': 'not-a-list'}
|
||||
primitives, problems = S.section(garbage, 600, 260)
|
||||
assert isinstance(primitives, list) and primitives
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
def test_section_degrades_when_panelsketch_unavailable(monkeypatch):
|
||||
monkeypatch.setattr(S, 'panelsketch', None)
|
||||
primitives, problems = S.section(_values(), 400, 300)
|
||||
assert primitives and primitives[0]['kind'] == 'text'
|
||||
assert problems
|
||||
|
||||
|
||||
# --------------------------------------------------------- _cut_fraction
|
||||
|
||||
def test_cut_fraction_defaults_and_clamps():
|
||||
assert S._cut_fraction({}) == 0.5
|
||||
assert S._cut_fraction({'_cut_frac': None}) == 0.5
|
||||
assert S._cut_fraction({'_cut_frac': 'garbage'}) == 0.5
|
||||
assert S._cut_fraction({'_cut_frac': '-1'}) == 0.0
|
||||
assert S._cut_fraction({'_cut_frac': '2'}) == 1.0
|
||||
assert S._cut_fraction({'_cut_frac': '0.37'}) == 0.37
|
||||
assert S._cut_fraction({'_cut_frac': 0.6}) == 0.6
|
||||
Reference in New Issue
Block a user