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,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for `vibe.pa_structural.page()` -- the Structural Model section.
|
||||
|
||||
Runs on plain host CPython via `conftest.py`'s `vibe` stub; no Revit
|
||||
involved and none was running for this test run. Checks the disciplines
|
||||
`pa_structural.py`'s docstring commits to:
|
||||
|
||||
* only block kinds from the fixed vocabulary are ever emitted;
|
||||
* every field carries a plain-English label and a hint;
|
||||
* every optional block states a sentence for its empty case, and a
|
||||
DIFFERENT sentence when the key is missing from `values` altogether
|
||||
("could not judge" vs "judged, found nothing" must never collapse);
|
||||
* no wind/seismic field ever carries a computed default -- they come
|
||||
back exactly what was typed, or blank;
|
||||
* a key `panelanalysisform` does not (yet) define degrades to a
|
||||
`problems` entry instead of raising or inventing a value;
|
||||
* `values` is never mutated.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from vibe import pa_structural
|
||||
|
||||
|
||||
_BLOCK_KINDS = set(['heading', 'columns', 'field', 'table', 'kv', 'sentence',
|
||||
'drawing', 'rule', 'space'])
|
||||
_EDITOR_KINDS = set(['text', 'num', 'check', 'choice'])
|
||||
|
||||
|
||||
def _walk(blocks):
|
||||
"""Yield every block, 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_keys(blocks):
|
||||
return [b['key'] for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
|
||||
|
||||
def _sentences(blocks):
|
||||
return [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
|
||||
|
||||
|
||||
# --------------------------------------------------------------- structure
|
||||
|
||||
def test_only_known_block_kinds():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in _BLOCK_KINDS, 'unknown block kind {0!r}'.format(
|
||||
b.get('kind'))
|
||||
|
||||
|
||||
def test_top_level_heading_is_structural_model():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
assert blocks[0] == {'kind': 'heading', 'text': 'Structural Model',
|
||||
'level': 1}
|
||||
|
||||
|
||||
def test_never_raises_on_empty_values():
|
||||
blocks, problems = pa_structural.page({})
|
||||
assert isinstance(blocks, list) and blocks
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
def test_never_raises_on_none_values():
|
||||
blocks, problems = pa_structural.page(None)
|
||||
assert isinstance(blocks, list) and blocks
|
||||
|
||||
|
||||
def test_never_raises_on_garbage_values():
|
||||
# Wrong types, weird keys -- page() must degrade, not crash.
|
||||
blocks, problems = pa_structural.page({
|
||||
'restraints': 12345,
|
||||
'conc_loads': None,
|
||||
'wind_speed_vult': object(),
|
||||
'supports_ft': '',
|
||||
})
|
||||
assert isinstance(blocks, list) and blocks
|
||||
|
||||
|
||||
def test_values_dict_not_mutated():
|
||||
values = {'restraints': '0, 10, 20', 'conc_loads': ''}
|
||||
before = dict(values)
|
||||
pa_structural.page(values)
|
||||
assert values == before
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ fields
|
||||
|
||||
def test_every_field_has_plain_english_label_and_hint():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
assert fields, 'expected at least one field block'
|
||||
for f in fields:
|
||||
assert f['label'], 'field {0!r} has no label'.format(f['key'])
|
||||
# A label that is just the raw key (no spaces, all lower/snake) is
|
||||
# not plain English -- every field must carry a written-out label.
|
||||
assert f['label'] != f['key']
|
||||
assert f['hint'], 'field {0!r} has no hint'.format(f['key'])
|
||||
assert f['editor'] in _EDITOR_KINDS
|
||||
assert isinstance(f['required'], bool)
|
||||
assert 'choices' in f
|
||||
if f['choices'] is not None:
|
||||
assert f['editor'] == 'choice'
|
||||
|
||||
|
||||
def test_symbol_is_beside_label_never_instead_of_it():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
by_key = dict((f['key'], f) for f in fields)
|
||||
# wind_speed_vult carries a real engineering symbol (Vult, mph) in
|
||||
# panelanalysisform -- the label must still be plain English, not the
|
||||
# symbol standing in for it.
|
||||
f = by_key['wind_speed_vult']
|
||||
assert f['label'].lower() != (f['symbol'] or '').lower()
|
||||
assert 'wind' in f['label'].lower() or 'speed' in f['label'].lower()
|
||||
|
||||
|
||||
def test_required_bearing_field_present():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
by_key = dict((f['key'], f) for f in fields)
|
||||
assert by_key['supports_ft']['required'] is True
|
||||
|
||||
|
||||
# -------------------------------------------------------- wind/seismic gate
|
||||
|
||||
def test_no_wind_or_seismic_field_carries_a_default_value():
|
||||
"""The owner's ruling, 2026-09-02: nothing computes wind yet, so every
|
||||
wind/seismic field is blank -- never a pre-filled pressure, Kz, or
|
||||
exposure guess -- regardless of what else is in `values`.
|
||||
"""
|
||||
blocks, _problems = pa_structural.page({
|
||||
'length_ft': '40', 'width_ft': '8', 'thickness_in': '6',
|
||||
})
|
||||
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
for f in fields:
|
||||
if f['key'].startswith('wind_') or f['key'].startswith('seismic_') \
|
||||
or f['key'].startswith('temp_'):
|
||||
assert 'not yet used' in (f['hint'] or '').lower() or \
|
||||
'nothing computes wind' in (f['hint'] or '').lower()
|
||||
|
||||
|
||||
def test_wind_and_seismic_sections_present():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
keys = _all_field_keys(blocks)
|
||||
for expected in ('wind_speed_vult', 'wind_risk_category',
|
||||
'wind_exposure', 'wind_enclosure', 'wind_asce_version',
|
||||
'wind_cc_chapter', 'wind_mwfrs_chapter',
|
||||
'wind_pressure_psf', 'wind_suction_psf', 'seismic_cs'):
|
||||
assert expected in keys
|
||||
|
||||
|
||||
# ------------------------------------------------------- empty-case sentences
|
||||
|
||||
def test_releases_missing_key_says_unknown_not_none():
|
||||
blocks, problems = pa_structural.page({}) # no 'releases' key at all
|
||||
sentences = _sentences(blocks)
|
||||
assert any('unknown' in s.lower() and 'release' in s.lower()
|
||||
for s in sentences)
|
||||
assert not any(s == 'Design contains no releases.' for s in sentences)
|
||||
assert any('releases' in p for p in problems)
|
||||
|
||||
|
||||
def test_releases_blank_key_says_design_contains_no_releases():
|
||||
blocks, problems = pa_structural.page({'releases': ' '})
|
||||
sentences = _sentences(blocks)
|
||||
assert 'Design contains no releases.' in sentences
|
||||
|
||||
|
||||
def test_releases_filled_key_renders_a_table_not_a_sentence():
|
||||
blocks, _problems = pa_structural.page({'releases': 'Tie 2, sideways'})
|
||||
tables = [b for b in _walk(blocks) if b.get('kind') == 'table']
|
||||
assert any('Release' in t['headers'] for t in tables)
|
||||
|
||||
|
||||
def test_exterior_soil_missing_vs_blank_are_different_sentences():
|
||||
blocks_missing, _ = pa_structural.page({})
|
||||
blocks_blank, _ = pa_structural.page({'ext_soil_loads': ''})
|
||||
sm = _sentences(blocks_missing)
|
||||
sb = _sentences(blocks_blank)
|
||||
assert 'Design contains no exterior soil loads.' in sb
|
||||
assert 'Design contains no exterior soil loads.' not in sm
|
||||
assert any('exterior soil loads' in s.lower() and 'unknown' in s.lower()
|
||||
for s in sm)
|
||||
|
||||
|
||||
def test_interior_soil_missing_vs_blank_are_different_sentences():
|
||||
blocks_missing, _ = pa_structural.page({})
|
||||
blocks_blank, _ = pa_structural.page({'int_soil_loads': ''})
|
||||
sm = _sentences(blocks_missing)
|
||||
sb = _sentences(blocks_blank)
|
||||
assert 'Design contains no interior soil loads.' in sb
|
||||
assert 'Design contains no interior soil loads.' not in sm
|
||||
|
||||
|
||||
def test_design_strips_blank_matches_eriksson_wording():
|
||||
blocks, _problems = pa_structural.page({'design_strips_rows': ''})
|
||||
sentences = _sentences(blocks)
|
||||
assert ('No design strips are defined; the entire cross section will '
|
||||
'be used.') in sentences
|
||||
|
||||
|
||||
def test_additional_self_weight_blank_vs_missing():
|
||||
blocks_missing, _ = pa_structural.page({})
|
||||
blocks_blank, _ = pa_structural.page({'addl_sw_magnitude_plf': ''})
|
||||
sm = _sentences(blocks_missing)
|
||||
sb = _sentences(blocks_blank)
|
||||
assert any('additional self weight' in s.lower() and 'unknown' in s.lower()
|
||||
for s in sm)
|
||||
assert any("panel's own concrete weight" in s.lower() for s in sb)
|
||||
assert sm != sb
|
||||
|
||||
|
||||
def test_concentrated_loads_blank_vs_missing():
|
||||
blocks_missing, _ = pa_structural.page({})
|
||||
blocks_blank, _ = pa_structural.page({'conc_loads': ''})
|
||||
sm = _sentences(blocks_missing)
|
||||
sb = _sentences(blocks_blank)
|
||||
assert any('point loads' in s.lower() and 'unknown' in s.lower()
|
||||
for s in sm)
|
||||
assert 'No concentrated point loads entered -- design assumes none.' \
|
||||
in sb
|
||||
|
||||
|
||||
def test_restraints_table_missing_vs_blank_vs_filled():
|
||||
blocks_missing, _ = pa_structural.page({})
|
||||
blocks_blank, _ = pa_structural.page({'restraints': ''})
|
||||
blocks_filled, _ = pa_structural.page({'restraints': '0, 10, 20'})
|
||||
|
||||
sm = _sentences(blocks_missing)
|
||||
assert any('restraint' in s.lower() and 'unknown' in s.lower()
|
||||
for s in sm)
|
||||
|
||||
sb = _sentences(blocks_blank)
|
||||
assert 'Design contains no restraints.' in sb
|
||||
|
||||
tables = [b for b in _walk(blocks_filled) if b.get('kind') == 'table']
|
||||
restraint_tables = [t for t in tables if 'Elevation (ft)' in t['headers']]
|
||||
assert restraint_tables
|
||||
assert len(restraint_tables[0]['rows']) == 3
|
||||
|
||||
|
||||
def test_restraint_rows_key_preferred_over_plain_restraints():
|
||||
blocks, _problems = pa_structural.page({
|
||||
'restraints': '0, 10',
|
||||
'restraint_rows': "0, Pin, y, y, -\n10'-1, Tieback, n, y, 12",
|
||||
})
|
||||
tables = [b for b in _walk(blocks) if b.get('kind') == 'table']
|
||||
restraint_tables = [t for t in tables if 'Elevation (ft)' in t['headers']]
|
||||
assert len(restraint_tables[0]['rows']) == 2
|
||||
# Type column (index 2) came from restraint_rows, not left blank the
|
||||
# way the plain-elevations fallback would leave it.
|
||||
assert restraint_tables[0]['rows'][0][2] == 'Pin'
|
||||
|
||||
|
||||
# ---------------------------------------------------- missing-key degrading
|
||||
|
||||
def test_missing_panelanalysisform_keys_become_problems_not_exceptions():
|
||||
blocks, problems = pa_structural.page({})
|
||||
assert problems, 'expected at least one problems sentence for the new keys'
|
||||
# every problems entry is a plain sentence, not a traceback fragment
|
||||
for p in problems:
|
||||
assert isinstance(p, str) or isinstance(p, type(u''))
|
||||
assert 'Traceback' not in p
|
||||
|
||||
|
||||
def test_new_keys_still_render_with_a_usable_label():
|
||||
blocks, problems = pa_structural.page({})
|
||||
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
by_key = dict((f['key'], f) for f in fields)
|
||||
# 'temp_ext_f' has no entry in panelanalysisform today -- must still
|
||||
# render with a real label and hint rather than being skipped.
|
||||
f = by_key['temp_ext_f']
|
||||
assert f['label'] == 'Exterior temperature'
|
||||
assert f['hint']
|
||||
|
||||
|
||||
def test_drawing_blocks_name_panelsketch_entry_points_only():
|
||||
blocks, _problems = pa_structural.page({})
|
||||
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
||||
sources = set(d['source'] for d in drawings)
|
||||
assert sources <= set(['elevation', 'plan', 'section', 'weight_diagram'])
|
||||
for d in drawings:
|
||||
assert d['caption']
|
||||
assert isinstance(d['w'], int) and isinstance(d['h'], int)
|
||||
|
||||
|
||||
def test_no_client_or_job_strings_leak_into_the_page():
|
||||
"""Nothing this page emits may carry an identity.
|
||||
|
||||
The obvious version of this test spells the forbidden strings out --
|
||||
and a leak detector that names the secret IS the leak, which is exactly
|
||||
why `.scrub-terms` is gitignored ("so the list of names is not itself
|
||||
the leak"). So this asks about SHAPE instead: an emitted string that
|
||||
looks like a job number, a run date, a source-file name or a licensee
|
||||
line is refused whatever it says.
|
||||
|
||||
The named-terms half of this belongs to `tools/scrub.py`, which reads
|
||||
that private list. This is the half that still works on a machine where
|
||||
the list is absent.
|
||||
"""
|
||||
blocks, problems = pa_structural.page({})
|
||||
text = repr(blocks) + repr(problems)
|
||||
assert not re.search(r'\d{5,}', text) # a job number
|
||||
assert not re.search(r'\d{2}/\d{2}/\d{4}', text) # a run date
|
||||
assert not re.search(r'\.(ewf|rvt)', text, re.I) # a source file
|
||||
assert 'licensee' not in text.lower()
|
||||
Reference in New Issue
Block a user