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,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Host tests for `vibe.pa_extents` -- the Panel Analysis form's "Concrete
|
||||
Extents" section layout: panel geometry, the openings table, the drawings
|
||||
row, and Section Properties.
|
||||
|
||||
Runs on plain CPython via `tests/conftest.py`'s `vibe` stub -- this module
|
||||
imports no Revit and no `panelsketch` (it only NAMES a `panelsketch` entry
|
||||
point in a `drawing` block's `source`; the renderer, not this module, calls
|
||||
it). What this suite checks, per the task brief: only known block `kind`s
|
||||
come out; every `field` block carries a plain-English label and a hint;
|
||||
`openings_rows` being absent degrades to a `problems` entry rather than a
|
||||
crash; Section Properties never prints a row it did not derive; and `values`
|
||||
is never mutated.
|
||||
"""
|
||||
from vibe import panelanalysisform as paf # noqa: F401 (registers the stub)
|
||||
from vibe import pa_extents as E
|
||||
|
||||
|
||||
ALLOWED_KINDS = {'heading', 'columns', 'field', 'kv', 'sentence', 'table',
|
||||
'drawing', 'rule', 'space'}
|
||||
|
||||
|
||||
def _walk(blocks):
|
||||
"""Yield every block in `blocks`, recursing into `columns`."""
|
||||
for b in blocks:
|
||||
yield b
|
||||
if b.get('kind') == 'columns':
|
||||
for col in b['columns']:
|
||||
for sub in _walk(col):
|
||||
yield sub
|
||||
|
||||
|
||||
def _all_field_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
|
||||
|
||||
def _all_drawing_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
||||
|
||||
|
||||
# --------------------------------------------------------------- vocabulary
|
||||
|
||||
def test_empty_values_only_known_kinds_no_crash():
|
||||
blocks, problems = E.page({})
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in ALLOWED_KINDS, 'unknown kind: {0!r}'.format(b)
|
||||
|
||||
|
||||
def test_none_values_does_not_raise():
|
||||
# page() must never raise -- an absent values dict is a valid call.
|
||||
blocks, problems = E.page(None)
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
def test_filled_values_only_known_kinds_no_crash():
|
||||
values = {
|
||||
'length_ft': '107', 'width_ft': '9', 'thickness_in': '8',
|
||||
'section_type': 'Solid', 'top_in_form': 'Interior',
|
||||
'top_elevation_ft': '107', 'bottom_elevation_ft': '0',
|
||||
'left_miter': 'no', 'right_miter': 'yes',
|
||||
'openings_rows': [
|
||||
{'type': 'opening', 'pos_along': 8.0 / 12.0,
|
||||
'pos_along_from': 'start', 'length': 6.875,
|
||||
'pos_cross': 2 + 11.5 / 12.0, 'pos_cross_from': 'left',
|
||||
'width': 3 + 4.5 / 12.0, 'pos_vert': None, 'height': None,
|
||||
'label': ''},
|
||||
],
|
||||
}
|
||||
blocks, problems = E.page(values)
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in ALLOWED_KINDS
|
||||
|
||||
|
||||
# ------------------------------------------------------------ field quality
|
||||
|
||||
def test_every_field_has_a_plain_label_and_a_hint():
|
||||
blocks, _problems = E.page({})
|
||||
fields = _all_field_blocks(blocks)
|
||||
assert fields, 'expected at least one field block'
|
||||
for f in fields:
|
||||
assert f['label'] and f['label'].strip(), \
|
||||
'field {0!r} has no plain-English label'.format(f['key'])
|
||||
assert f['hint'] and f['hint'].strip(), \
|
||||
'field {0!r} has no hint'.format(f['key'])
|
||||
assert f['editor'] in ('text', 'num', 'check', 'choice')
|
||||
assert isinstance(f['required'], bool)
|
||||
|
||||
|
||||
def test_known_paf_fields_use_panelanalysisform_label_verbatim():
|
||||
# Checked LIVE against panelanalysisform.FIELDS rather than a hardcoded
|
||||
# key list, because a sibling agent is restructuring that module WHILE
|
||||
# this suite runs -- `left_miter` / `right_miter` landed there mid-task
|
||||
# (see the module docstring's "read defensively" discipline). Any key
|
||||
# `_paf_field` finds must be reflected verbatim; any key it does not
|
||||
# find is covered by the MISSING test below instead.
|
||||
blocks, _problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
candidates = ('length_ft', 'width_ft', 'thickness_in', 'section_type',
|
||||
'top_in_form', 'left_miter', 'right_miter',
|
||||
'top_elevation_ft', 'bottom_elevation_ft')
|
||||
for key in candidates:
|
||||
if key not in fields:
|
||||
continue
|
||||
if E._paf_field(key) is None:
|
||||
continue # MISSING from panelanalysisform -- not this test's job
|
||||
assert fields[key]['label'] == paf.label_of(key), \
|
||||
'{0} EXISTS in panelanalysisform but label was not taken from it' \
|
||||
.format(key)
|
||||
assert fields[key]['hint'] == (paf.help_of(key) or fields[key]['hint'])
|
||||
|
||||
|
||||
def test_keys_missing_from_panelanalysisform_are_named_in_problems():
|
||||
# Some Eriksson concepts on this page are not yet fields
|
||||
# panelanalysisform.FIELDS declares -- MISSING, per the task brief,
|
||||
# never an error. The page still draws a slot for them (to match
|
||||
# Eriksson's own layout) but must say, in `problems`, that a value
|
||||
# typed there is not wired up yet. Checked live against `_paf_field`
|
||||
# rather than a hardcoded list, since the sibling agent restructuring
|
||||
# panelanalysisform may add any of these keys before this suite is
|
||||
# read again.
|
||||
blocks, problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
candidate_keys = ('top_elevation_ft', 'bottom_elevation_ft',
|
||||
'left_miter', 'right_miter', 'top_in_form_check',
|
||||
'top_in_form_width_in', 'top_in_form_height_in')
|
||||
joined = ' '.join(problems)
|
||||
still_missing = [k for k in candidate_keys if E._paf_field(k) is None]
|
||||
assert still_missing, \
|
||||
'expected at least one of {0} to still be MISSING from ' \
|
||||
'panelanalysisform for this test to mean anything'.format(candidate_keys)
|
||||
for key in still_missing:
|
||||
assert key in fields, '{0} should still get a field slot'.format(key)
|
||||
assert repr(key) in joined, \
|
||||
'{0} MISSING from panelanalysisform but not named in problems' \
|
||||
.format(key)
|
||||
|
||||
|
||||
def test_length_ft_is_required_and_matches_paf():
|
||||
blocks, _problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
assert fields['length_ft']['required'] is True
|
||||
assert fields['width_ft']['required'] is True
|
||||
assert fields['thickness_in']['required'] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- miters
|
||||
|
||||
def test_miter_blank_prints_no_sentence():
|
||||
blocks, _problems = E.page({})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert not any('miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
def test_miter_explicit_no_prints_a_sentence_not_a_blank():
|
||||
blocks, _problems = E.page({'left_miter': 'no'})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert any('Left miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
def test_miter_explicit_yes_prints_no_sentence():
|
||||
blocks, _problems = E.page({'right_miter': 'yes'})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert not any('Right miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
# -------------------------------------------------------------- openings
|
||||
|
||||
def test_openings_rows_absent_degrades_with_a_problem_not_a_crash():
|
||||
blocks, problems = E.page({'openings': ''})
|
||||
assert any('openings_rows' in p for p in problems)
|
||||
# still lays out a sentence, never a broken table
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in ALLOWED_KINDS
|
||||
|
||||
|
||||
def test_openings_rows_absent_but_raw_text_present_is_shown_verbatim():
|
||||
blocks, problems = E.page({'openings': '2, 1, 3, 4, Door'})
|
||||
assert any('openings_rows' in p for p in problems)
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert any('2, 1, 3, 4, Door' in s for s in sentences)
|
||||
|
||||
|
||||
def test_openings_rows_present_builds_grouped_table():
|
||||
rows = [
|
||||
{'type': 'opening', 'pos_along': 8.0 / 12.0,
|
||||
'pos_along_from': 'start', 'length': 6.0 + 10.5 / 12.0,
|
||||
'pos_cross': 2.0 + 11.5 / 12.0, 'pos_cross_from': 'left',
|
||||
'width': 3.0 + 4.5 / 12.0, 'pos_vert': None, 'height': None,
|
||||
'label': ''},
|
||||
{'type': 'reveal', 'pos_along': None, 'pos_along_from': None,
|
||||
'length': None, 'pos_cross': None, 'pos_cross_from': None,
|
||||
'width': None, 'pos_vert': None, 'height': None, 'label': ''},
|
||||
]
|
||||
blocks, problems = E.page({'openings_rows': rows})
|
||||
assert not any('openings_rows' in p for p in problems)
|
||||
tables = [b for b in _walk(blocks) if b['kind'] == 'table']
|
||||
assert len(tables) == 1
|
||||
table = tables[0]
|
||||
assert table['headers'] == ['Type', 'Position', 'Length',
|
||||
'Horizontal Position', 'Width',
|
||||
'Vertical Position', 'Height']
|
||||
assert table['header_groups'] == [('Position along Length', 2),
|
||||
('Position in Cross Section', 4)]
|
||||
assert len(table['rows']) == 2
|
||||
first = table['rows'][0]
|
||||
assert first[0] == 'Opening'
|
||||
assert first[1] == '8" from Start'
|
||||
assert first[3] == "2'-11 1/2\" from Left"
|
||||
assert first[5] == '-' # pos_vert blank -> dash
|
||||
assert first[6] == '-' # height blank -> dash
|
||||
second = table['rows'][1]
|
||||
assert second[0] == 'Reveal'
|
||||
assert second[1] == '-' # blank position -> plain dash, no "from ..."
|
||||
|
||||
|
||||
def test_openings_rows_empty_list_is_a_real_answer_not_missing():
|
||||
blocks, problems = E.page({'openings_rows': []})
|
||||
assert not any('openings_rows' in p for p in problems)
|
||||
tables = [b for b in _walk(blocks) if b['kind'] == 'table']
|
||||
assert len(tables) == 1
|
||||
assert tables[0]['rows'] == []
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert any('No openings' in s for s in sentences)
|
||||
|
||||
|
||||
def test_unknown_opening_type_is_echoed_not_invented():
|
||||
rows = [{'type': 'blockout', 'pos_along': None, 'pos_along_from': None,
|
||||
'length': None, 'pos_cross': None, 'pos_cross_from': None,
|
||||
'width': None, 'pos_vert': None, 'height': None, 'label': ''}]
|
||||
blocks, _problems = E.page({'openings_rows': rows})
|
||||
table = [b for b in _walk(blocks) if b['kind'] == 'table'][0]
|
||||
assert table['rows'][0][0] == 'Blockout' # title-cased, not remapped
|
||||
|
||||
|
||||
# ------------------------------------------------------------ section props
|
||||
|
||||
def test_section_properties_never_invents_a_derived_row():
|
||||
blocks, problems = E.page({'width_ft': '9', 'thickness_in': '8'})
|
||||
kvs = [b for b in _walk(blocks) if b['kind'] == 'kv']
|
||||
assert kvs, 'expected a Section Properties kv block once width+thickness given'
|
||||
labels = [row[0] for row in kvs[0]['rows']]
|
||||
assert labels == ['Area, in²', 'Width, in', 'Height, in']
|
||||
forbidden = ('Cx', 'Cy', 'Izz', 'Iyy', 'Izy', 'Sb', 'St')
|
||||
for row_label, _value in kvs[0]['rows']:
|
||||
for bad in forbidden:
|
||||
assert not row_label.startswith(bad)
|
||||
joined = ' '.join(problems)
|
||||
for bad in forbidden:
|
||||
assert bad in joined, \
|
||||
'{0} should be named in problems as refused, not silently dropped'.format(bad)
|
||||
|
||||
|
||||
def test_section_properties_values_are_correct():
|
||||
blocks, _problems = E.page({'width_ft': '9', 'thickness_in': '8'})
|
||||
kv = [b for b in _walk(blocks) if b['kind'] == 'kv'][0]
|
||||
rows = dict(kv['rows'])
|
||||
assert rows['Width, in'] == '108'
|
||||
assert rows['Height, in'] == '8'
|
||||
assert rows['Area, in²'] == '864'
|
||||
|
||||
|
||||
def test_section_properties_missing_inputs_never_prints_a_kv_block():
|
||||
blocks, problems = E.page({})
|
||||
kvs = [b for b in _walk(blocks) if b['kind'] == 'kv']
|
||||
assert kvs == []
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert any('waiting on' in s for s in sentences)
|
||||
assert any('could not be derived' in p for p in problems)
|
||||
|
||||
|
||||
# -------------------------------------------------------------- drawings
|
||||
|
||||
def test_drawings_use_only_named_panelsketch_entry_points():
|
||||
blocks, _problems = E.page({})
|
||||
drawings = _all_drawing_blocks(blocks)
|
||||
assert drawings, 'expected at least the Cross Section drawing'
|
||||
allowed_sources = {'sketch', 'elevation', 'plan', 'section',
|
||||
'weight_diagram'}
|
||||
for d in drawings:
|
||||
assert d['source'] in allowed_sources
|
||||
assert isinstance(d['w'], int) and d['w'] > 0
|
||||
assert isinstance(d['h'], int) and d['h'] > 0
|
||||
assert d['caption'] and d['caption'].strip()
|
||||
|
||||
|
||||
def test_side_and_front_elevation_limitation_is_named_in_problems():
|
||||
blocks, problems = E.page({})
|
||||
captions = [d['caption'] for d in _all_drawing_blocks(blocks)]
|
||||
assert 'Side Elevation' in captions
|
||||
assert 'Front Elevation' in captions
|
||||
assert 'Cross Section' in captions
|
||||
assert any('same combined image' in p for p in problems)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ no mutation
|
||||
|
||||
def test_values_dict_is_never_mutated():
|
||||
values = {'length_ft': '107', 'width_ft': '9', 'thickness_in': '8',
|
||||
'openings': 'garbage'}
|
||||
before = dict(values)
|
||||
E.page(values)
|
||||
assert values == before
|
||||
|
||||
|
||||
def test_page_never_raises_on_garbage_values():
|
||||
garbage = {
|
||||
'width_ft': 'not-a-number', 'thickness_in': [1, 2, 3],
|
||||
'openings_rows': 'not-a-list', 'left_miter': object(),
|
||||
'openings': None,
|
||||
}
|
||||
blocks, problems = E.page(garbage)
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- utilities
|
||||
|
||||
def test_feet_str_formats_like_eriksson():
|
||||
assert E._feet_str(8.0 / 12.0) == '8"'
|
||||
assert E._feet_str(0.0) == '0"'
|
||||
assert E._feet_str(20.0 + 8.0 / 12.0) == "20'-8\""
|
||||
assert E._feet_str(2.0 + 11.5 / 12.0) == "2'-11 1/2\""
|
||||
|
||||
|
||||
def test_dim_or_dash_passes_through_preformatted_strings():
|
||||
assert E._dim_or_dash("2'-11.5\"") == "2'-11.5\""
|
||||
assert E._dim_or_dash('') == '-'
|
||||
assert E._dim_or_dash(None) == '-'
|
||||
|
||||
|
||||
def test_tri_bool_blank_is_none_not_false():
|
||||
assert E._tri_bool('') is None
|
||||
assert E._tri_bool(None) is None
|
||||
assert E._tri_bool('no') is False
|
||||
assert E._tri_bool('yes') is True
|
||||
Reference in New Issue
Block a user