25592fa6c2
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.
475 lines
20 KiB
Python
475 lines
20 KiB
Python
# -*- 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
|