52d94303bc
ci / test (push) Has been cancelled
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.
339 lines
14 KiB
Python
339 lines
14 KiB
Python
# -*- 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
|