# -*- coding: utf-8 -*- """Tests for `vibe.pa_reinforcement.page` -- the Reinforcement section of the Panel Analysis input form. Runs on plain CPython via `tests/conftest.py`'s import trick (no Revit, no pyRevit, no WPF -- Revit is NOT running for this suite). Covers the doctrine the module docstring commits to: * only block kinds from the fixed vocabulary are ever emitted; * every 'field' block carries a plain-English label and a hint; * every optional sub-block (strands / rebar / mesh / interrupts) states an empty-case sentence when it has nothing, never a blank; * a missing `values['strand_rows']` degrades to the raw-text fallback with a `problems` entry, rather than raising; * no summary quantity (area, cgx, ex, ey) is ever invented; * `values` is never mutated. """ import copy from vibe import pa_reinforcement as par from vibe import panelanalysisform as pf _KNOWN_KINDS = set(['heading', 'columns', 'field', 'table', 'kv', 'sentence', 'drawing', 'rule', 'space']) def _walk(blocks): """Yield every block, recursing into 'columns' children.""" for b in blocks: yield b if b.get('kind') == 'columns': for col in b.get('columns') or []: for sub in _walk(col): yield sub def _fields(blocks): return [b 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'] # ------------------------------------------------------------------- shape def test_only_known_block_kinds_empty_values(): blocks, problems = par.page({}) for b in _walk(blocks): assert b.get('kind') in _KNOWN_KINDS, b def test_only_known_block_kinds_filled_values(): values = { 'strand_rail_location': 'Left edge', 'strand_rail_offset_in': '0', 'ps_count': '4', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000', 'ps_loss_pct': '8', 'ps_rows': '2 @ 2\n2 @ 6 @ 70', 'mild_as': '#4 @ 12" o.c.', 'mesh': '6x6 W1.4xW1.4', } blocks, problems = par.page(values) for b in _walk(blocks): assert b.get('kind') in _KNOWN_KINDS, b def test_page_never_raises_on_garbage_values(): garbage = {'strand_rows': 'not a list', 'ps_rows': 12345, 'ps_dia_in': None, 'mild_as': object()} blocks, problems = par.page(garbage) assert isinstance(blocks, list) assert isinstance(problems, list) def test_page_never_raises_on_none_values(): blocks, problems = par.page(None) assert isinstance(blocks, list) # ------------------------------------------------------------- field labels def test_every_field_has_plain_english_label_and_hint(): blocks, _ = par.page({'ps_rows': '2 @ 2'}) fields = _fields(blocks) assert fields, 'expected at least one field block' for f in fields: assert f['label'] and isinstance(f['label'], str) # A plain-English label is not just the raw key repeated back. assert f['label'] != f['key'] assert f['hint'], 'field {0!r} has no hint'.format(f['key']) def test_field_blocks_reference_real_panelanalysisform_keys(): blocks, problems = par.page({}) known_keys = set(f['key'] for f in pf.FIELDS) for f in _fields(blocks): assert f['key'] in known_keys, ( '{0!r} is not a real panelanalysisform key'.format(f['key'])) # ---------------------------------------------------------- empty-case text def test_strands_blank_prints_no_prestressing_sentence(): blocks, _ = par.page({}) assert 'Design contains no prestressing' in _sentences(blocks) def test_rebar_blank_prints_no_rebar_sentence(): blocks, _ = par.page({}) assert 'Design contains no rebar' in _sentences(blocks) def test_mesh_blank_prints_no_mesh_sentence(): blocks, _ = par.page({}) assert 'Design contains no mesh' in _sentences(blocks) def test_interrupts_always_could_not_judge_never_no_interrupts(): """No field exists for interrupts, so this must never claim "none".""" blocks, problems = par.page({}) sentences = _sentences(blocks) assert any('could not judge' in s.lower() for s in sentences) assert 'Design contains no interrupts' not in sentences assert any('interrupts' in p.lower() for p in problems) def test_rebar_filled_has_no_empty_sentence(): blocks, _ = par.page({'mild_as': '#4 @ 12" o.c. each face'}) assert 'Design contains no rebar' not in _sentences(blocks) def test_mesh_filled_has_no_empty_sentence(): blocks, _ = par.page({'mesh': '6x6 W1.4xW1.4'}) assert 'Design contains no mesh' not in _sentences(blocks) def test_strands_filled_has_no_empty_sentence_and_has_table(): blocks, _ = par.page({'ps_rows': '2 @ 2 @ 70', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000'}) assert 'Design contains no prestressing' not in _sentences(blocks) tables = [b for b in blocks if b['kind'] == 'table'] assert len(tables) == 1 assert tables[0]['rows'] # ------------------------------------------------------- strand_rows intake def test_structured_strand_rows_used_when_present_no_fallback_problem(): values = { 'strand_rows': [{'count': 2, 'depth_in': 2.0, 'pull_pct': 70.0}], 'ps_dia_in': '1/2', 'ps_pull_lb': '30000', } blocks, problems = par.page(values) table = [b for b in blocks if b['kind'] == 'table'][0] assert table['rows'] == [['1/2', '0.153', '1/2', '2', '70%', '2"']] assert not any('parsed the raw strand-group text' in p for p in problems) def test_missing_strand_rows_falls_back_to_ps_rows_text_with_problem(): values = {'ps_rows': '2 @ 2 @ 70', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000'} blocks, problems = par.page(values) table = [b for b in blocks if b['kind'] == 'table'][0] assert table['rows'] and table['rows'][0][3] == '2' assert any('ps_rows' in p for p in problems) def test_missing_strand_rows_and_blank_ps_rows_is_quiet_no_prestressing(): """Absent structured input AND blank text is a real "nothing typed yet" answer, not an error -- must not appear in `problems`.""" blocks, problems = par.page({}) assert 'Design contains no prestressing' in _sentences(blocks) assert not any('ps_rows' in p or 'strand_rows' in p for p in problems) def test_malformed_strand_rows_text_reported_not_silently_dropped(): blocks, problems = par.page({'ps_rows': 'garbage line with no at-sign'}) assert any('could not' in p.lower() for p in problems) assert 'Design contains no prestressing' in _sentences(blocks) def test_malformed_structured_strand_rows_entry_skipped_with_problem(): values = {'strand_rows': [{'count': 'oops', 'depth_in': 2.0}]} blocks, problems = par.page(values) assert any('skipped it' in p for p in problems) assert 'Design contains no prestressing' in _sentences(blocks) # --------------------------------------------------------- never fabricated def test_unknown_strand_size_refused_not_guessed(): values = {'ps_rows': '2 @ 2', 'ps_dia_in': 'made-up-size', 'ps_pull_lb': '30000'} blocks, problems = par.page(values) table = [b for b in blocks if b['kind'] == 'table'][0] for row in table['rows']: assert row[1] == '?' # area column never guessed assert any('no section area is known' in p for p in problems) kvs = [b for b in blocks if b['kind'] == 'kv'] assert not kvs # no Aps/cgy summary without an area def test_strand_summary_never_prints_cgx_ex_ey_as_numbers(): values = {'ps_rows': '2 @ 2 @ 70\n2 @ 6', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000'} blocks, problems = par.page(values) kv = [b for b in blocks if b['kind'] == 'kv'][0] rows_by_label = dict(kv['rows']) assert 'not shown' in rows_by_label['cgx, in'] assert 'not shown' in rows_by_label['ex, in'] assert 'not shown' in rows_by_label['ey, in'] # Aps and cgy ARE real derived numbers, not refused. assert rows_by_label['Aps, in²'].replace('.', '').isdigit() assert any('cgx' in p and 'not computed' in p for p in problems) def test_aps_and_cgy_derived_correctly_from_entered_rows(): values = {'ps_rows': '2 @ 2\n2 @ 6', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000'} blocks, _ = par.page(values) kv = [b for b in blocks if b['kind'] == 'kv'][0] rows_by_label = dict(kv['rows']) area = 0.153 expected_aps = 4 * area expected_cgy = (2 * 2.0 + 2 * 6.0) / 4.0 assert abs(float(rows_by_label['Aps, in²']) - expected_aps) < 1e-6 assert abs(float(rows_by_label['cgy, in (depth to CG)']) - expected_cgy) < 1e-6 def test_no_spacing_column_in_strand_table(): """Horizontal strand spacing is not an input on this form -- the table must not imply it is known.""" blocks, _ = par.page({'ps_rows': '2 @ 2', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000'}) table = [b for b in blocks if b['kind'] == 'table'][0] assert not any('spac' in h.lower() for h in table['headers']) # --------------------------------------------------------------- no mutation def test_values_dict_not_mutated(): values = { 'strand_rail_location': 'Left edge', 'strand_rail_offset_in': '0', 'ps_rows': '2 @ 2 @ 70', 'ps_dia_in': '1/2', 'ps_pull_lb': '30000', 'mild_as': '', 'mesh': '', } before = copy.deepcopy(values) par.page(values) assert values == before def test_missing_field_key_reports_problem_not_crash(monkeypatch): """A field this module asks panelanalysisform for that no longer exists there degrades to a MISSING problem, never a crash.""" real_spec = pf._spec def _fake_spec(key): if key == 'mesh': return None return real_spec(key) monkeypatch.setattr(pf, '_spec', _fake_spec) blocks, problems = par.page({}) assert any('mesh' in p and 'MISSING' in p for p in problems) # The section still renders -- heading + empty sentence, just no field. assert 'Design contains no mesh' in _sentences(blocks) # -------------------------------------------------------------- drawing tag def test_drawing_block_names_section_source_never_calls_panelsketch(): blocks, _ = par.page({}) drawings = [b for b in blocks if b['kind'] == 'drawing'] assert len(drawings) == 1 assert drawings[0]['source'] == 'section' assert drawings[0]['caption'] assert isinstance(drawings[0]['w'], int) assert isinstance(drawings[0]['h'], int)