25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
677 lines
28 KiB
Python
677 lines
28 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.
|
|
|
|
2026-09-04 REWRITE, matching the owner's headless-render ruling the same
|
|
day (see `pa_structural.py`'s module docstring for the ruling text):
|
|
restraints, concentrated loads and wind zones are now EDITABLE TABLES, not
|
|
free-text fields; every ASCE wind input is gone, replaced by a `wind_mode`
|
|
choice and a manually-typed `wind_zone` table; Seismic / Temperature /
|
|
Soil are collapsed `section` blocks; and the old four-panel
|
|
`source: 'elevation'` drawing is gone, replaced by the `pa_sketch_freebody`
|
|
free-body block.
|
|
|
|
Checks the disciplines both this rewrite and the standing docstring commit
|
|
to:
|
|
|
|
* only block kinds from the fixed vocabulary are ever emitted;
|
|
* every field carries a plain-English label and a hint;
|
|
* every optional free-text block still states a DIFFERENT sentence for
|
|
"key missing" vs. "key present but blank" ("could not judge" vs.
|
|
"judged, found nothing" must never collapse);
|
|
* no wind/seismic/temperature field ever carries a computed default;
|
|
* a key `panelanalysisform` does not (yet) define degrades to a
|
|
`problems` entry instead of raising or inventing a value;
|
|
* `values` is never mutated;
|
|
* every ASCE-only wind key is gone from this page;
|
|
* `restraint` / `conc_load` / `wind_zone` each render as an
|
|
`editable_table` with the exact contract column shape;
|
|
* Seismic / Temperature Loads / Soil Loads are collapsed sections;
|
|
* no inline (outside a collapsed section) non-quiet sentence runs longer
|
|
than one sentence.
|
|
"""
|
|
|
|
import re
|
|
|
|
from vibe import pa_structural
|
|
|
|
|
|
_BLOCK_KINDS = set(['heading', 'columns', 'field', 'table', 'editable_table',
|
|
'section', 'kv', 'sentence', 'drawing', 'rule', 'space'])
|
|
_EDITOR_KINDS = set(['text', 'num', 'check', 'choice'])
|
|
|
|
_ASCE_GONE_KEYS = ('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')
|
|
|
|
|
|
def _walk(blocks):
|
|
"""Yield every block, recursing into 'columns' and 'section'."""
|
|
for b in blocks:
|
|
yield b
|
|
if b.get('kind') == 'columns':
|
|
for col in b['columns']:
|
|
for sub in _walk(col):
|
|
yield sub
|
|
elif b.get('kind') == 'section':
|
|
for sub in _walk(b['blocks']):
|
|
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']
|
|
|
|
|
|
def _editable_tables(blocks):
|
|
return dict((b['table'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'editable_table')
|
|
|
|
|
|
def _sections(blocks):
|
|
return dict((b['title'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'section')
|
|
|
|
|
|
# --------------------------------------------------------------- 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, malformed row-table keys -- page() must
|
|
# degrade, not crash.
|
|
blocks, problems = pa_structural.page({
|
|
'conc_loads': None,
|
|
'wind_speed_vult': object(),
|
|
'supports_ft': '',
|
|
'restraint.1.present': 12345,
|
|
'restraint.x.present': 'yes',
|
|
'restraint..present': 'yes',
|
|
'wind_zone.2.present': object(),
|
|
3: 'not even a string key',
|
|
})
|
|
assert isinstance(blocks, list) and blocks
|
|
|
|
|
|
def test_values_dict_not_mutated():
|
|
values = {'releases': '', 'restraint.1.present': 'yes',
|
|
'restraint.1.elevation_ft': '0'}
|
|
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)
|
|
# temp_alpha_t carries a real engineering symbol in panelanalysisform's
|
|
# fallback -- the label must still be plain English, not the symbol
|
|
# standing in for it.
|
|
f = by_key['temp_alpha_t']
|
|
assert f['label'].lower() != (f['symbol'] or '').lower()
|
|
assert 'thermal' in f['label'].lower() or \
|
|
'expansion' in f['label'].lower()
|
|
|
|
|
|
def test_required_bearing_table_present():
|
|
# Owner ruling, 2026-09-05: `supports_ft` is no longer rendered as its
|
|
# own `field` -- bearing positions are the `support` editable row
|
|
# table now (a fallback for an old saved values file, never shown).
|
|
blocks, _problems = pa_structural.page({})
|
|
tables = [b for b in _walk(blocks) if b.get('kind') == 'editable_table']
|
|
by_table = dict((t['table'], t) for t in tables)
|
|
assert 'support' in by_table
|
|
assert by_table['support']['columns'][0]['col'] == 'x_ft'
|
|
fields = [b for b in _walk(blocks) if b.get('kind') == 'field']
|
|
assert 'supports_ft' not in [f['key'] for f in fields]
|
|
|
|
|
|
def test_old_restraint_dropdown_fields_are_gone():
|
|
"""Owner, 2026-09-04: restraints must be an editable table, not the
|
|
foundation-connection dropdown + floor-tie checkbox this page used to
|
|
render instead of one."""
|
|
blocks, _problems = pa_structural.page({})
|
|
keys = _all_field_keys(blocks)
|
|
assert 'restraint_foundation_connection' not in keys
|
|
assert 'restraint_floor_tie' not in keys
|
|
|
|
|
|
# --------------------------------------------------------------- wind gate
|
|
|
|
def test_no_asce_wind_fields_remain():
|
|
"""Owner, 2026-09-04: '前面那些应该在 project-config 里面,从这里移除' --
|
|
every ASCE-only wind input is gone from this page."""
|
|
blocks, _problems = pa_structural.page({})
|
|
keys = set(_all_field_keys(blocks))
|
|
for key in _ASCE_GONE_KEYS:
|
|
assert key not in keys, '{0} should have been removed'.format(key)
|
|
|
|
|
|
def test_wind_mode_field_is_a_manual_vs_project_config_choice():
|
|
blocks, _problems = pa_structural.page({})
|
|
fields = dict((b['key'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'field')
|
|
f = fields['wind_mode']
|
|
assert f['editor'] == 'choice'
|
|
assert f['choices'] == ['Manual', 'From project config']
|
|
|
|
|
|
def test_wind_project_config_not_built_sentence_is_quiet():
|
|
blocks, _problems = pa_structural.page({})
|
|
sentences = [b for b in _walk(blocks) if b.get('kind') == 'sentence']
|
|
hits = [b for b in sentences if 'not built yet' in b['text'].lower()
|
|
and 'project config' in b['text'].lower()]
|
|
assert hits, 'expected a quiet sentence about project-config not being built'
|
|
for b in hits:
|
|
assert b.get('quiet') is True
|
|
|
|
|
|
def test_no_wind_or_seismic_or_temp_field_carries_a_default_value():
|
|
"""The owner's ruling, 2026-09-02 (and again 2026-09-04 for wind): "
|
|
nothing computes wind/seismic yet, so every such field is blank --
|
|
never a pre-filled pressure or 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('seismic_') or f['key'].startswith('temp_'):
|
|
assert 'not yet used' in (f['hint'] or '').lower()
|
|
|
|
|
|
# --------------------------------------------------------- editable tables
|
|
|
|
def test_restraint_table_shape():
|
|
blocks, _problems = pa_structural.page({})
|
|
tables = _editable_tables(blocks)
|
|
t = tables['restraint']
|
|
assert t['add_label'] == 'Add restraint'
|
|
cols = dict((c['col'], c) for c in t['columns'])
|
|
assert cols['elevation_ft']['editor'] == 'num'
|
|
assert cols['type']['editor'] == 'choice'
|
|
assert cols['type']['choices'] == ['Pin', 'Fixed', 'Free', 'Floor tie']
|
|
assert cols['gravity']['editor'] == 'check'
|
|
assert cols['thermal']['editor'] == 'check'
|
|
assert cols['stiffness_kin']['editor'] == 'num'
|
|
assert list(cols.keys()) == ['elevation_ft', 'type', 'gravity',
|
|
'thermal', 'stiffness_kin']
|
|
|
|
|
|
def test_conc_load_table_shape():
|
|
blocks, _problems = pa_structural.page({})
|
|
tables = _editable_tables(blocks)
|
|
t = tables['conc_load']
|
|
assert t['add_label'] == 'Add load'
|
|
cols = dict((c['col'], c) for c in t['columns'])
|
|
assert cols['label']['editor'] == 'text'
|
|
assert cols['elevation_ft']['editor'] == 'num'
|
|
assert cols['offset_in']['editor'] == 'num'
|
|
assert cols['dead_k']['editor'] == 'num'
|
|
assert cols['live_k']['editor'] == 'num'
|
|
assert list(cols.keys()) == ['label', 'elevation_ft', 'offset_in',
|
|
'dead_k', 'live_k']
|
|
|
|
|
|
def test_wind_zone_table_shape():
|
|
blocks, _problems = pa_structural.page({})
|
|
tables = _editable_tables(blocks)
|
|
t = tables['wind_zone']
|
|
assert t['add_label'] == 'Add wind zone'
|
|
cols = dict((c['col'], c) for c in t['columns'])
|
|
assert cols['label']['editor'] == 'text'
|
|
assert cols['from_elev_ft']['editor'] == 'num'
|
|
assert cols['to_elev_ft']['editor'] == 'num'
|
|
assert cols['width_ft']['editor'] == 'num'
|
|
assert cols['pressure_psf']['editor'] == 'num'
|
|
assert cols['suction_psf']['editor'] == 'num'
|
|
assert list(cols.keys()) == ['label', 'from_elev_ft', 'to_elev_ft',
|
|
'width_ft', 'pressure_psf', 'suction_psf']
|
|
|
|
|
|
def test_editable_tables_are_declarations_not_snapshots():
|
|
"""The table blocks never carry row data of their own -- they name the
|
|
table and its columns; the renderer reads live rows straight out of
|
|
`values` via `row_ids`/`row_key` so an editable cell is a real binding,
|
|
not a copy the owner correctly rejected as 'locked'."""
|
|
blocks, _problems = pa_structural.page({
|
|
'restraint.1.present': 'yes', 'restraint.1.elevation_ft': '0',
|
|
'restraint.1.type': 'Pin'})
|
|
tables = _editable_tables(blocks)
|
|
assert 'rows' not in tables['restraint']
|
|
|
|
|
|
# ------------------------------------------------------- free-body drawing
|
|
|
|
def test_freebody_drawing_block_matches_contract_exactly():
|
|
"""`values={}` has no conc_load/wind_zone rows, so `_freebody_aspect`
|
|
-- see its own tests below -- lands on the 0.16 baseline, not the old
|
|
flat 0.22."""
|
|
blocks, _problems = pa_structural.page({})
|
|
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
|
fb = [d for d in drawings if d.get('module') == 'pa_sketch_freebody']
|
|
assert len(fb) == 1
|
|
d = fb[0]
|
|
assert d['source'] == 'freebody'
|
|
assert d['full_width'] is True
|
|
assert d['aspect'] == 0.16
|
|
assert d['caption'] == 'Free body: restraints and loads'
|
|
# 'support' added 2026-09-05 (the bearing-position row table); 'supports_ft'
|
|
# kept for a saved panel with no table rows.
|
|
assert set(d['depends_on']) == set(['restraint', 'support', 'conc_load',
|
|
'wind_zone', 'bottom_elevation_ft',
|
|
'top_elevation_ft', 'supports_ft'])
|
|
|
|
|
|
# ------------------------------------------------------- free-body aspect
|
|
|
|
def _row_values(table, n):
|
|
return {'{0}.{1}.present'.format(table, n): 'yes',
|
|
'{0}.{1}.elevation_ft'.format(table, n): str(n)}
|
|
|
|
|
|
def test_freebody_aspect_is_016_with_no_load_or_wind_rows():
|
|
assert pa_structural._freebody_aspect({}) == 0.16
|
|
|
|
|
|
def test_freebody_aspect_grows_003_per_load_or_wind_row():
|
|
values = {}
|
|
values.update(_row_values('conc_load', 1))
|
|
assert abs(pa_structural._freebody_aspect(values) - 0.19) < 1e-9
|
|
values.update(_row_values('wind_zone', 1))
|
|
assert abs(pa_structural._freebody_aspect(values) - 0.22) < 1e-9
|
|
|
|
|
|
def test_freebody_aspect_caps_at_028():
|
|
values = {}
|
|
for n in range(1, 4):
|
|
values.update(_row_values('conc_load', n))
|
|
for n in range(1, 4):
|
|
values.update(_row_values('wind_zone', n))
|
|
# 6 rows * 0.03 + 0.16 = 0.34, clamped down to 0.28
|
|
assert pa_structural._freebody_aspect(values) == 0.28
|
|
|
|
|
|
def test_freebody_drawing_aspect_matches_helper_with_rows_present():
|
|
values = {}
|
|
values.update(_row_values('conc_load', 1))
|
|
blocks, _problems = pa_structural.page(values)
|
|
fb = [b for b in _walk(blocks) if b.get('kind') == 'drawing'
|
|
and b.get('module') == 'pa_sketch_freebody'][0]
|
|
assert fb['aspect'] == pa_structural._freebody_aspect(values)
|
|
|
|
|
|
def test_old_four_panel_elevation_drawing_is_gone():
|
|
"""Owner, 2026-09-04: the free body replaces the old drawing that
|
|
crammed front/side/section-properties/cross-section onto one canvas
|
|
here."""
|
|
blocks, _problems = pa_structural.page({})
|
|
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
|
sources = [d.get('source') for d in drawings]
|
|
assert 'elevation' not in sources
|
|
captions = [d.get('caption') for d in drawings]
|
|
assert not any('restraint and support elevations' in (c or '').lower()
|
|
for c in captions)
|
|
|
|
|
|
def test_old_weight_diagram_drawing_is_gone():
|
|
"""Same-day follow-up ruling: the free body above already draws every
|
|
point load and the member itself, so the old `weight_diagram` drawing
|
|
block under Additional Self Weight -- a second, tiny picture repeating
|
|
a subset of the same data -- is removed entirely, not merely hidden."""
|
|
blocks, _problems = pa_structural.page({})
|
|
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
|
sources = [d.get('source') for d in drawings]
|
|
assert 'weight_diagram' not in sources
|
|
# exactly one drawing block on the whole page: the free body.
|
|
assert len(drawings) == 1
|
|
|
|
|
|
# --------------------------------------------------------- collapsed sections
|
|
|
|
def test_seismic_temperature_soil_are_collapsed_sections():
|
|
"""Owner, 2026-09-04: '"seismic, temp, soil 这几个板块先折叠掉'."""
|
|
blocks, _problems = pa_structural.page({})
|
|
sections = _sections(blocks)
|
|
assert set(sections.keys()) == set([
|
|
'Seismic', 'Temperature Loads', 'Soil Loads',
|
|
'Additional Self Weight', 'Design Strips'])
|
|
for title, sec in sections.items():
|
|
assert sec['collapsed'] is True, '{0} should be collapsed'.format(title)
|
|
assert isinstance(sec['blocks'], list) and sec['blocks']
|
|
|
|
|
|
def test_additional_self_weight_and_design_strips_are_collapsed_sections():
|
|
"""Same-day follow-up: neither block is built yet (nothing in the
|
|
solve reads either), so both fold the same way Seismic/Temperature/
|
|
Soil already do -- not left open and prominent in the inline flow."""
|
|
blocks, _problems = pa_structural.page({'addl_sw_magnitude_plf': ''})
|
|
top_level_titles = set(
|
|
b.get('text') for b in blocks
|
|
if b.get('kind') == 'heading' and b.get('level') == 2)
|
|
assert 'Additional Self Weight' not in top_level_titles
|
|
assert 'Design Strips' not in top_level_titles
|
|
|
|
sections = _sections(blocks)
|
|
# `design_strips_method` renders unconditionally; the additional-self-
|
|
# weight fields only once the key is present (see
|
|
# `test_additional_self_weight_fields_appear_once_a_magnitude_is_typed`
|
|
# for the 'missing' vs. 'blank' vs. 'filled' distinction) -- blank here
|
|
# so both sections' fields are checkable in one call.
|
|
asw_keys = _all_field_keys(sections['Additional Self Weight']['blocks'])
|
|
ds_keys = _all_field_keys(sections['Design Strips']['blocks'])
|
|
assert 'addl_sw_start_in' in asw_keys
|
|
assert 'design_strips_method' in ds_keys
|
|
|
|
|
|
def test_additional_self_weight_fields_appear_once_a_magnitude_is_typed():
|
|
blocks, _problems = pa_structural.page(
|
|
{'addl_sw_magnitude_plf': '25'})
|
|
sections = _sections(blocks)
|
|
asw_keys = _all_field_keys(sections['Additional Self Weight']['blocks'])
|
|
assert 'addl_sw_start_in' in asw_keys
|
|
assert 'addl_sw_end_in' in asw_keys
|
|
|
|
|
|
def test_soil_section_keeps_the_exterior_interior_columns_layout():
|
|
blocks, _problems = pa_structural.page({})
|
|
sections = _sections(blocks)
|
|
soil = sections['Soil Loads']
|
|
columns_blocks = [b for b in soil['blocks'] if b.get('kind') == 'columns']
|
|
assert columns_blocks
|
|
assert len(columns_blocks[0]['columns']) == 2
|
|
|
|
|
|
def test_seismic_and_temperature_fields_live_inside_their_sections():
|
|
blocks, _problems = pa_structural.page({})
|
|
top_level_keys = _all_field_keys(
|
|
[b for b in blocks if b.get('kind') != 'section'])
|
|
assert 'seismic_cs' not in top_level_keys
|
|
assert 'temp_ext_f' not in top_level_keys
|
|
|
|
sections = _sections(blocks)
|
|
seismic_keys = _all_field_keys(sections['Seismic']['blocks'])
|
|
temp_keys = _all_field_keys(sections['Temperature Loads']['blocks'])
|
|
assert 'seismic_cs' in seismic_keys
|
|
assert 'temp_ext_f' in temp_keys
|
|
|
|
|
|
# --------------------------------------------------------------- quiet prose
|
|
|
|
def test_no_long_inline_non_quiet_helper_prose():
|
|
"""Grey helper prose must be quiet (routed to the collapsed Notes
|
|
panel); a functional one-sentence status line is fine either way. Only
|
|
the INLINE flow is checked here -- content already folded inside a
|
|
collapsed `section` is not inline by definition."""
|
|
blocks, _problems = pa_structural.page({})
|
|
inline = [b for b in blocks if b.get('kind') != 'section']
|
|
for b in _walk(inline):
|
|
if b.get('kind') != 'sentence':
|
|
continue
|
|
if b.get('quiet'):
|
|
continue
|
|
text = b['text']
|
|
# crude one-sentence check: at most one terminal period, and it is
|
|
# the last character (never "sentence one. sentence two.")
|
|
assert text.count('.') <= 1, \
|
|
'non-quiet inline sentence looks like more than one ' \
|
|
'sentence: {0!r}'.format(text)
|
|
|
|
|
|
def test_restraints_overview_sentence_is_quiet():
|
|
blocks, _problems = pa_structural.page({})
|
|
sentences = [b for b in _walk(blocks) if b.get('kind') == 'sentence']
|
|
hits = [b for b in sentences if 'tied back' in b['text'].lower()]
|
|
assert hits
|
|
assert hits[0].get('quiet') is True
|
|
|
|
|
|
# ------------------------------------------------------- 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_seismic_apply_to_is_a_choice_field():
|
|
blocks, _problems = pa_structural.page({})
|
|
fields = dict((b['key'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'field')
|
|
f = fields['seismic_apply_to']
|
|
assert f['editor'] == 'choice'
|
|
assert f['choices'] == ['None', 'Out-of-plane', 'In-plane', 'Both']
|
|
|
|
|
|
def test_load_opt_offset_from_choices():
|
|
blocks, _problems = pa_structural.page({})
|
|
fields = dict((b['key'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'field')
|
|
f = fields['load_opt_offset_from']
|
|
assert f['editor'] == 'choice'
|
|
assert f['choices'] == ['Deflection Section', 'Top in Form',
|
|
'Bottom in Form']
|
|
|
|
|
|
def test_load_opt_pattern_live_is_a_checkbox():
|
|
blocks, _problems = pa_structural.page({})
|
|
fields = dict((b['key'], b) for b in _walk(blocks)
|
|
if b.get('kind') == 'field')
|
|
assert fields['load_opt_pattern_live']['editor'] == 'check'
|
|
|
|
|
|
# ---------------------------------------------------- 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'
|
|
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']
|
|
|
|
|
|
# ------------------------------------------------------------- row_ids/row_key
|
|
|
|
def test_row_key_shape():
|
|
assert pa_structural.row_key('restraint', '1', 'elevation_ft') == \
|
|
'restraint.1.elevation_ft'
|
|
assert pa_structural.row_key('conc_load', 'u3', 'dead_k') == \
|
|
'conc_load.u3.dead_k'
|
|
|
|
|
|
def test_row_ids_reads_present_yes_and_sorts_numerically():
|
|
values = {
|
|
'restraint.2.present': 'yes', 'restraint.2.elevation_ft': '10',
|
|
'restraint.10.present': 'yes', 'restraint.10.elevation_ft': '30',
|
|
'restraint.1.present': 'yes', 'restraint.1.elevation_ft': '0',
|
|
'restraint.5.present': 'no', # not 'yes' -- excluded
|
|
'restraint.5.elevation_ft': '99',
|
|
'conc_load.1.present': 'yes', # a different table -- excluded
|
|
}
|
|
assert pa_structural.row_ids('restraint', values) == [1, 2, 10]
|
|
|
|
|
|
def test_row_ids_ignores_malformed_keys():
|
|
values = {'restraint.x.present': 'yes', 'restraint..present': 'yes',
|
|
'restraint.1.2.present': 'yes', 'restraint.3.present': 'yes'}
|
|
assert pa_structural.row_ids('restraint', values) == [3]
|
|
|
|
|
|
def test_row_ids_empty_on_empty_values():
|
|
assert pa_structural.row_ids('restraint', {}) == []
|
|
assert pa_structural.row_ids('restraint', None) == []
|
|
|
|
|
|
# ------------------------------------------------------------------ leaks
|
|
|
|
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()
|
|
|
|
|
|
# ---------------------------------------------------------------- issue #22
|
|
|
|
def test_free_body_is_the_last_block_on_the_page():
|
|
# Owner, 2026-09-07 (issue #22, item 6b): "把 free body 的图挪到页面的
|
|
# 最底下" -- after every collapsed section, so everything it draws is
|
|
# entered above it. Top-level order, not the recursive walk: the
|
|
# drawing must be the final entry of `page()`'s own list.
|
|
blocks, _problems = pa_structural.page({})
|
|
last = blocks[-1]
|
|
assert last['kind'] == 'drawing' and last['source'] == 'freebody'
|
|
drawings = [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
|
assert len(drawings) == 1
|
|
# and the collapsed sections all sit before it
|
|
idx_last_section = max(i for i, b in enumerate(blocks)
|
|
if b.get('kind') == 'section')
|
|
assert idx_last_section < len(blocks) - 1
|
|
|
|
|
|
def test_support_and_restraint_tables_carry_an_empty_state():
|
|
# Issue #22, items 5/11: an empty `support` table must say why it is
|
|
# empty -- the read's own `supports_basis` / `restraints_basis`
|
|
# sentence when there is one, else a could-not-judge sentence -- never
|
|
# a bare "Add support" button. The `support` one also says the solve
|
|
# needs at least two rows, so the gap shows before Run, not after.
|
|
blocks, _problems = pa_structural.page({})
|
|
tables = _editable_tables(blocks)
|
|
sup = tables['support']['empty_state']
|
|
assert sup['basis_keys'][0] == 'supports'
|
|
assert 'at least two' in sup['text']
|
|
assert 'could-not-judge' in sup['text']
|
|
res = tables['restraint']['empty_state']
|
|
assert res['basis_keys'][0] == 'restraints'
|
|
# tables with no model read (conc_load, wind_zone) carry none: the
|
|
# page's generic "nothing entered yet" line covers them
|
|
assert 'empty_state' not in tables['conc_load']
|