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,278 @@
|
||||
# -*- 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)
|
||||
Reference in New Issue
Block a user