Files
willtools-demo/WillTools.extension/tests/test_pa_structural.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
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.
2026-09-03 16:33:47 -05:00

313 lines
12 KiB
Python

# -*- 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()