52d94303bc
ci / test (push) Has been cancelled
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.
279 lines
10 KiB
Python
279 lines
10 KiB
Python
# -*- 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)
|