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