Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Host tests for `vibe.pa_handling` -- the Panel Analysis form's Handling
|
||||
section, rendered as generic blocks for the WPF page renderer.
|
||||
|
||||
Loaded via the sibling `conftest.py` trick, same as `test_panelanalysisform.py`
|
||||
-- plain CPython, no Revit, no pyRevit. Covers the things the module's own
|
||||
docstring promises: only the fixed block vocabulary comes out, "does not
|
||||
apply" and "not filled in" render as different cell shapes, an unticked
|
||||
stage never drops or re-gates a value the user already typed, every field and
|
||||
matrix row carries a plain-English label and a hint, a missing
|
||||
`panelanalysisform` accessor degrades to a `problems` sentence instead of
|
||||
raising, and `values` is never mutated.
|
||||
"""
|
||||
import copy
|
||||
|
||||
from vibe import knowledge # noqa: F401 (registers the vibe stub)
|
||||
from vibe import panelanalysisform as paf
|
||||
from vibe import pa_handling
|
||||
|
||||
|
||||
# The fixed vocabulary `pa_handling.page()` may emit. Anything else is a bug
|
||||
# -- the generic renderer draws an unknown `kind` as nothing, so a typo here
|
||||
# is a silently missing section rather than a crash, which is exactly why
|
||||
# this is asserted rather than assumed.
|
||||
_KNOWN_KINDS = set([
|
||||
'heading', 'columns', 'field', 'matrix', 'table', 'legend', 'kv',
|
||||
'sentence', 'drawing', 'rule', 'space',
|
||||
])
|
||||
|
||||
|
||||
def _walk(blocks):
|
||||
"""Flatten every block, including nested 'columns' children."""
|
||||
for b in blocks:
|
||||
yield b
|
||||
if b.get('kind') == 'columns':
|
||||
for col in b.get('columns') or []:
|
||||
for sub in _walk(col):
|
||||
yield sub
|
||||
|
||||
|
||||
def _matrix_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'matrix']
|
||||
|
||||
|
||||
def _field_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
|
||||
|
||||
# --------------------------------------------------------------- fixtures
|
||||
|
||||
def _some_values():
|
||||
"""A plausible in-progress form: a couple of fields filled, most blank."""
|
||||
values = {}
|
||||
for key in paf.stage_field_keys():
|
||||
values[key] = ''
|
||||
for f in paf.FIELDS:
|
||||
values[f['key']] = ''
|
||||
values['weight_lb'] = '32000'
|
||||
values[paf.stage_field_key('Stripping', 'include')] = 'yes'
|
||||
values[paf.stage_field_key('Stripping', 'load_multiplier')] = '1.4'
|
||||
values[paf.stage_field_key('Primary Erection', 'rigging')] = 'Auto'
|
||||
values[paf.stage_field_key('Primary Erection', 'sling_angle')] = '60'
|
||||
# A stage the user switched OFF but already typed a value into --
|
||||
# exactly the case the "unticked stage keeps values" rule protects.
|
||||
values[paf.stage_field_key('Secondary Erection', 'include')] = 'no'
|
||||
values[paf.stage_field_key('Secondary Erection', 'load_multiplier')] = '2.0'
|
||||
return values
|
||||
|
||||
|
||||
# --------------------------------------------------------------- vocabulary
|
||||
|
||||
def test_every_block_kind_is_known():
|
||||
# `problems` is expected to be non-empty here -- see the module
|
||||
# docstring's list of gaps (no centroid, no top-edge-lifter field, no
|
||||
# dunnage field, no lifting-configuration drawing source). Those are
|
||||
# honest, permanent gaps, not bugs; this test only checks the block
|
||||
# vocabulary itself.
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
for b in _walk(blocks):
|
||||
assert 'kind' in b
|
||||
assert b['kind'] in _KNOWN_KINDS, b
|
||||
|
||||
|
||||
def test_empty_values_still_renders_known_kinds_only():
|
||||
values = dict((k, '') for k in paf.stage_field_keys())
|
||||
for f in paf.FIELDS:
|
||||
values[f['key']] = ''
|
||||
blocks, _problems = pa_handling.page(values)
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in _KNOWN_KINDS, b
|
||||
|
||||
|
||||
def test_page_never_raises_on_none_values():
|
||||
# page() must degrade, never raise, even with nothing handed in.
|
||||
blocks, problems = pa_handling.page(None)
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
# ---------------------------------------------------- not-applicable vs blank
|
||||
|
||||
def test_stage_matrix_shape():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
matrices = _matrix_blocks(blocks)
|
||||
assert len(matrices) == 1
|
||||
m = matrices[0]
|
||||
assert m['columns'] == list(paf.STAGES)
|
||||
assert len(m['rows']) == len(paf.STAGE_OPTIONS)
|
||||
for row in m['rows']:
|
||||
assert len(row['cells']) == len(paf.STAGES)
|
||||
|
||||
|
||||
def test_erection_only_option_is_na_for_stripping_and_transportation():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
columns = m['columns']
|
||||
rigging_row = next(r for r in m['rows']
|
||||
if r['cells'] is not None and
|
||||
'rigging configuration' in r['label'].lower())
|
||||
for stage_name in ('Stripping', 'Transportation'):
|
||||
idx = columns.index(stage_name)
|
||||
assert rigging_row['cells'][idx] == {'na': True}
|
||||
for stage_name in ('Primary Erection', 'Secondary Erection'):
|
||||
idx = columns.index(stage_name)
|
||||
cell = rigging_row['cells'][idx]
|
||||
assert cell != {'na': True}
|
||||
assert cell is not None
|
||||
assert 'editor' in cell
|
||||
|
||||
|
||||
def test_na_cell_and_unfilled_cell_render_differently():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
columns = m['columns']
|
||||
rigging_row = next(r for r in m['rows']
|
||||
if 'rigging configuration' in r['label'].lower())
|
||||
load_mult_row = next(r for r in m['rows']
|
||||
if 'load multiplier' in r['label'].lower())
|
||||
na_cell = rigging_row['cells'][columns.index('Stripping')]
|
||||
# Load Multiplier applies to every stage, so Stripping's cell there is
|
||||
# an ordinary (currently unfilled, from the caller's point of view)
|
||||
# editable cell -- shape must differ from the NA cell above.
|
||||
unfilled_cell = load_mult_row['cells'][columns.index('Transportation')]
|
||||
assert na_cell == {'na': True}
|
||||
assert 'na' not in unfilled_cell
|
||||
assert unfilled_cell['editor'] == 'num'
|
||||
|
||||
|
||||
def test_include_row_never_na():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
include_row = next(r for r in m['rows'] if 'include' in r['label'].lower())
|
||||
for cell in include_row['cells']:
|
||||
assert cell != {'na': True}
|
||||
assert cell['editor'] == 'check'
|
||||
|
||||
|
||||
# ------------------------------------------- unticked stage keeps its values
|
||||
|
||||
def test_excluded_stage_cell_still_present_and_editable():
|
||||
"""Secondary Erection is switched off in `_some_values()` but its
|
||||
load_multiplier was typed in; the matrix must still offer that cell as
|
||||
an ordinary editable cell (never NA, never dropped) -- the caller's
|
||||
values dict is what actually decides required-ness at validate() time,
|
||||
not this block, and the cell key must still be there for the renderer
|
||||
to read/write against."""
|
||||
values = _some_values()
|
||||
blocks, _problems = pa_handling.page(values)
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
columns = m['columns']
|
||||
load_mult_row = next(r for r in m['rows'] if 'load multiplier' in
|
||||
r['label'].lower())
|
||||
cell = load_mult_row['cells'][columns.index('Secondary Erection')]
|
||||
assert cell != {'na': True}
|
||||
assert cell is not None
|
||||
assert cell['key'] == paf.stage_field_key('Secondary Erection',
|
||||
'load_multiplier')
|
||||
# And the value the user typed is still sitting in `values`, untouched.
|
||||
assert values[cell['key']] == '2.0'
|
||||
|
||||
|
||||
def test_values_dict_is_never_mutated():
|
||||
values = _some_values()
|
||||
before = copy.deepcopy(values)
|
||||
pa_handling.page(values)
|
||||
assert values == before
|
||||
|
||||
|
||||
# --------------------------------------------------------- labels and hints
|
||||
|
||||
def test_every_field_block_has_label_and_hint():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
for f in _field_blocks(blocks):
|
||||
assert f.get('label'), f
|
||||
assert f.get('hint'), f
|
||||
|
||||
|
||||
def test_every_matrix_row_has_label_and_hint():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
for row in m['rows']:
|
||||
assert row.get('label'), row
|
||||
# 'include' is the only row Eriksson gives a very short help line
|
||||
# to, but every STAGE_OPTIONS row does carry one -- assert it rides
|
||||
# through rather than getting dropped in translation.
|
||||
assert row.get('hint'), row
|
||||
|
||||
|
||||
def test_symbol_rides_beside_label_not_instead_of_it():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
load_mult_row = next(r for r in m['rows'] if 'load multiplier' in
|
||||
r['label'].lower())
|
||||
# The symbol (x self-weight) must be ADDITIONAL to the plain words, per
|
||||
# the em-dash join `_row_label` uses -- never a bare symbol standing in
|
||||
# for the label.
|
||||
assert load_mult_row['label'].lower().startswith('load multiplier')
|
||||
assert u'—' in load_mult_row['label'] # em dash
|
||||
|
||||
|
||||
# --------------------------------------------------- missing-accessor degrade
|
||||
|
||||
def test_missing_stage_accessor_degrades_to_problem_not_exception():
|
||||
# Build the values dict BEFORE removing STAGES -- `stage_field_keys()`
|
||||
# itself reads the module-global `STAGES`, so this must not depend on
|
||||
# the attribute we are about to take away.
|
||||
values = _some_values()
|
||||
had_stages = hasattr(paf, 'STAGES')
|
||||
saved = getattr(paf, 'STAGES', None)
|
||||
try:
|
||||
if had_stages:
|
||||
delattr(paf, 'STAGES')
|
||||
blocks, problems = pa_handling.page(values)
|
||||
assert isinstance(blocks, list)
|
||||
assert any('STAGES' in p for p in problems)
|
||||
# The rest of the page still renders around the hole.
|
||||
assert any(b.get('kind') == 'heading' and b.get('text') == 'Handling'
|
||||
for b in blocks)
|
||||
finally:
|
||||
if had_stages:
|
||||
paf.STAGES = saved
|
||||
|
||||
|
||||
def test_missing_stage_field_key_degrades_to_problem():
|
||||
values = _some_values()
|
||||
had = hasattr(paf, 'stage_field_key')
|
||||
saved = getattr(paf, 'stage_field_key', None)
|
||||
try:
|
||||
if had:
|
||||
delattr(paf, 'stage_field_key')
|
||||
blocks, problems = pa_handling.page(values)
|
||||
assert any('stage_field_key' in p for p in problems)
|
||||
assert not _matrix_blocks(blocks)
|
||||
finally:
|
||||
if had:
|
||||
paf.stage_field_key = saved
|
||||
|
||||
|
||||
def test_missing_weight_field_degrades_to_problem():
|
||||
had = 'FIELDS' in vars(paf)
|
||||
saved = paf.FIELDS
|
||||
try:
|
||||
paf.FIELDS = [f for f in paf.FIELDS if f['key'] != 'weight_lb']
|
||||
blocks, problems = pa_handling.page(_some_values())
|
||||
assert any('weight_lb' in p for p in problems)
|
||||
# kv block still renders (weight row falls back to the "not
|
||||
# entered" sentence), it does not vanish or raise.
|
||||
kvs = [b for b in _walk(blocks) if b['kind'] == 'kv']
|
||||
assert kvs
|
||||
finally:
|
||||
if had:
|
||||
paf.FIELDS = saved
|
||||
|
||||
|
||||
def test_no_top_edge_lifter_or_dunnage_field_is_a_known_gap():
|
||||
"""These are genuinely absent from panelanalysisform today (only
|
||||
face_lifters / edge_lifters exist) -- confirm the page says so rather
|
||||
than inventing a field or silently going blank."""
|
||||
blocks, problems = pa_handling.page(_some_values())
|
||||
assert any('top edge lifters' in p for p in problems)
|
||||
assert any('dunnage' in p for p in problems)
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert 'Design contains no top edge lifters.' in sentences
|
||||
assert 'Design contains no dunnage entered.' in sentences
|
||||
|
||||
|
||||
# --------------------------------------------------------------- real keys
|
||||
|
||||
def test_matrix_cell_keys_match_panelanalysisform_stage_field_key():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
m = _matrix_blocks(blocks)[0]
|
||||
for opt, row in zip(paf.STAGE_OPTIONS, m['rows']):
|
||||
opt_key = opt[0]
|
||||
for stage, cell in zip(m['columns'], row['cells']):
|
||||
if cell is None or cell.get('na'):
|
||||
continue
|
||||
assert cell['key'] == paf.stage_field_key(stage, opt_key)
|
||||
|
||||
|
||||
def test_load_factor_and_lifter_fields_use_real_keys():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
keys = set(f['key'] for f in _field_blocks(blocks))
|
||||
assert 'weight_lb' in keys
|
||||
assert 'self_weight_factor' in keys
|
||||
assert 'face_lifters' in keys
|
||||
assert 'edge_lifters' in keys
|
||||
|
||||
|
||||
def test_drawing_blocks_name_real_sibling_sources_only():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
drawings = [b for b in _walk(blocks) if b['kind'] == 'drawing']
|
||||
sources = set(d['source'] for d in drawings)
|
||||
# 'plan' and 'weight_diagram' are the two panelsketch entry points the
|
||||
# sibling agent added; pa_handling must never invent a third.
|
||||
assert sources <= set(['plan', 'weight_diagram'])
|
||||
assert 'plan' in sources
|
||||
for d in drawings:
|
||||
assert d.get('caption')
|
||||
assert isinstance(d.get('w'), int)
|
||||
assert isinstance(d.get('h'), int)
|
||||
|
||||
|
||||
def test_legend_groups_match_documented_vocabulary():
|
||||
blocks, _problems = pa_handling.page(_some_values())
|
||||
legends = [b for b in _walk(blocks) if b['kind'] == 'legend']
|
||||
assert len(legends) == 1
|
||||
names = [g[0] for g in legends[0]['groups']]
|
||||
assert names == ['Shapes', 'Fills']
|
||||
|
||||
|
||||
def test_import_failure_degrades_cleanly(monkeypatch):
|
||||
import vibe.pa_handling as mod
|
||||
|
||||
def _boom():
|
||||
return None, 'vibe.panelanalysisform could not be imported (boom)'
|
||||
|
||||
monkeypatch.setattr(mod, '_import_paf', _boom)
|
||||
blocks, problems = mod.page(_some_values())
|
||||
assert problems == ['vibe.panelanalysisform could not be imported (boom)']
|
||||
assert any(b['kind'] == 'heading' for b in blocks)
|
||||
Reference in New Issue
Block a user