Files
willtools-demo/WillTools.extension/tests/test_pa_criteria.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
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.
2026-09-03 16:33:47 -05:00

339 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Tests for `vibe.pa_criteria.page` -- the Design Criteria section layout.
No Revit and no WPF renderer are available here (see `conftest.py`'s
docstring for why `vibe` imports this way), so this suite cannot check that
the page LOOKS right -- nobody has run this inside Revit and this file does
not claim otherwise. What it can and does check mechanically:
* `page()` never raises, on an empty dict, a filled dict, `None` values,
`None` reads, or a `values` object that is not even a dict.
* every block returned is a well-formed member of the block vocabulary
`page()`'s docstring promises (`heading`, `columns`, `field`, `kv`,
`sentence`, `table`, `rule`, `space`) -- an unknown `kind` renders as
nothing on the real form, so a typo here would be a silently missing
block that only this test would catch.
* every `field` block carries a non-empty, plain-English `label` and a
non-empty `hint` -- the owner's whole complaint about the old form was
that a field carried only its engineering symbol.
* a field key with no backing entry in `panelanalysisform.FIELDS` still
renders (an empty editor), and is named in `problems` -- never silently
dropped.
* `values` is never mutated by a call to `page()`.
* the block list is stable: calling `page()` twice on the same input
produces the same output.
"""
import copy
import sys
from pathlib import Path
TESTS_DIR = Path(__file__).resolve().parent
if str(TESTS_DIR) not in sys.path:
sys.path.insert(0, str(TESTS_DIR))
from vibe import pa_criteria as pc # noqa: E402
KNOWN_KINDS = set(['heading', 'columns', 'field', 'kv', 'sentence', 'table',
'rule', 'space'])
KNOWN_EDITORS = set(['text', 'num', 'check', 'choice'])
def _walk(blocks):
"""Yield every block in the tree, including nested `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 _assert_well_formed(blocks):
for b in blocks:
assert isinstance(b, dict), 'block is not a dict: {0!r}'.format(b)
kind = b.get('kind')
assert kind in KNOWN_KINDS, 'unknown block kind {0!r}'.format(kind)
if kind == 'heading':
assert isinstance(b['text'], str) and b['text'].strip()
assert b['level'] in (1, 2, 3)
elif kind == 'columns':
assert isinstance(b['columns'], list) and b['columns']
for col in b['columns']:
assert isinstance(col, list)
_assert_well_formed(col)
elif kind == 'field':
assert isinstance(b['key'], str) and b['key']
assert isinstance(b['label'], str) and b['label'].strip(), (
'field {0!r} has no plain-English label'.format(b['key']))
assert isinstance(b['hint'], str) and b['hint'].strip(), (
'field {0!r} has no hint'.format(b['key']))
assert b['symbol'] is None or isinstance(b['symbol'], str)
assert isinstance(b['required'], bool)
assert b['editor'] in KNOWN_EDITORS, (
'field {0!r} has unknown editor {1!r}'
.format(b['key'], b['editor']))
if b['editor'] == 'choice':
assert b['choices'], (
'choice field {0!r} has no choices'.format(b['key']))
else:
assert b['choices'] is None
elif kind == 'kv':
assert isinstance(b['rows'], list)
for row in b['rows']:
assert len(row) == 2
elif kind == 'sentence':
assert isinstance(b['text'], str) and b['text'].strip()
elif kind == 'table':
assert isinstance(b['headers'], list) and b['headers']
assert isinstance(b['rows'], list)
for row in b['rows']:
assert len(row) == len(b['headers']), (
'table row {0!r} does not match headers {1!r}'
.format(row, b['headers']))
for cell in row:
if isinstance(cell, dict):
assert cell.get('editor') == 'check'
assert cell.get('key')
else:
assert isinstance(cell, str)
elif kind == 'rule':
assert set(b.keys()) == set(['kind'])
elif kind == 'space':
assert isinstance(b['h'], int)
def _field_keys(blocks):
return [b['key'] for b in _walk(blocks) if b.get('kind') == 'field']
# --------------------------------------------------------------------- tests
def test_empty_values_never_raises_and_is_well_formed():
blocks, problems = pc.page({})
assert isinstance(blocks, list) and blocks
assert isinstance(problems, list)
_assert_well_formed(blocks)
def test_none_values_and_none_reads_never_raises():
blocks, problems = pc.page(None, None)
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_non_dict_values_degrades_rather_than_raising():
blocks, problems = pc.page('not a dict')
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_filled_values_never_raises_and_is_well_formed():
values = {
'fc_psi': '6000', 'fci_psi': '3500', 'unit_weight_pcf': '150',
'concrete_type': 'Normalweight', 'rebar_fy_ksi': '60',
'rebar_es_ksi': '29000', 'ps_fpu_ksi': '270', 'strand_ep_ksi': '28500',
'ps_loss_method': 'PCI', 'ps_loss_humidity_pct': '75',
'design_criteria.rvalue_calc_enabled': 'yes',
'design_criteria.min_reinf_checked': 'true',
}
blocks, problems = pc.page(values, {'mark': 'sample_mark',
'design_number': 'PNL1.0'})
assert isinstance(blocks, list) and blocks
_assert_well_formed(blocks)
def test_values_dict_is_never_mutated():
values = {'fc_psi': '6000'}
before = copy.deepcopy(values)
pc.page(values, {'mark': 'sample_mark'})
assert values == before
def test_reads_dict_is_never_mutated():
reads = {'mark': 'sample_mark', 'design_number': 'PNL1.0'}
before = copy.deepcopy(reads)
pc.page({}, reads)
assert reads == before
def test_block_list_is_stable_for_the_same_input():
values = {'fc_psi': '6000', 'unit_weight_pcf': '150'}
reads = {'mark': 'sample_mark'}
a, pa = pc.page(values, reads)
b, pb = pc.page(values, reads)
assert a == b
assert pa == pb
def test_missing_key_degrades_to_a_field_and_names_a_problem():
blocks, problems = pc.page({})
keys = set(_field_keys(blocks))
# A key this file invented because Eriksson's page has no home for it
# yet in panelanalysisform.FIELDS.
assert 'design_criteria.analysis_beta_d' in keys
joined = ' '.join(problems)
assert 'design_criteria.analysis_beta_d' in joined
def test_existing_form_key_is_not_reported_as_missing():
blocks, problems = pc.page({})
joined = ' '.join(problems)
# fc_psi is a real, long-standing panelanalysisform.FIELDS key.
assert "'fc_psi'" not in joined or 'fc_psi) has no backing' not in joined
for p in problems:
assert 'fc_psi)' not in p
def test_every_field_key_appears_at_most_once():
blocks, _problems = pc.page({})
keys = _field_keys(blocks)
dupes = [k for k in set(keys) if keys.count(k) > 1]
assert not dupes, 'field key(s) rendered more than once: {0}'.format(dupes)
def test_section_heading_is_present():
blocks, _problems = pc.page({})
headings = [b['text'] for b in blocks if b.get('kind') == 'heading']
assert 'Design Criteria' in headings
assert headings[0] == 'Design Criteria'
def test_prime_notation_used_not_ascii_apostrophe():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') != 'field':
continue
symbol = b.get('symbol') or ''
# f'c / f'ci style ASCII apostrophes must never appear; the real
# prime character (U+2032) is what the renderer turns into a
# subscript.
assert "f'c" not in symbol and "f'ci" not in symbol
if u"f′" in symbol or u"E′" in symbol:
assert True # sanity: the prime character is reachable at all
def test_load_combination_tables_have_include_checkboxes():
blocks, problems = pc.page({})
tables = [b for b in _walk(blocks) if b.get('kind') == 'table']
assert len(tables) == 2
for t in tables:
assert t['headers'][-1] == 'Include'
for row in t['rows']:
cell = row[-1]
assert isinstance(cell, dict) and cell['editor'] == 'check'
joined = ' '.join(problems)
assert 'load_combo.strength' in joined
assert 'load_combo.service' in joined
def test_soil_blocks_are_sentences_not_editors():
blocks, problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('no exterior soil loads' in s for s in sentences)
assert any('no interior soil loads' in s for s in sentences)
assert any('soil loads are not collected' in p for p in problems)
def test_prestress_loss_group_is_a_sentence_when_blank():
blocks, _problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('No prestress loss assumptions' in s for s in sentences)
# And its fields must NOT be rendered while blank -- shown only once
# something in the group is filled in.
keys = set(_field_keys(blocks))
assert 'ps_loss_method' not in keys
def test_prestress_loss_group_becomes_fields_when_filled():
blocks, _problems = pc.page({'ps_loss_method': 'PCI'})
keys = set(_field_keys(blocks))
assert 'ps_loss_method' in keys
assert 'ps_loss_humidity_pct' in keys
def test_filling_one_group_field_does_not_file_a_phantom_problem_for_others():
# Regression guard: `_group_or_sentence` must not evaluate the
# filled-in branch (and thus call `F(key)`, which files a "missing"
# problem) when the sentence branch is the one actually shown.
_blocks, problems_blank = pc.page({})
joined = ' '.join(problems_blank)
assert 'ps_loss_method' not in joined
assert 'ps_loss_humidity_pct' not in joined
def test_rvalue_and_min_reinforcement_default_sentences_present():
blocks, _problems = pc.page({})
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
assert any('R-value is not being calculated' in s for s in sentences)
assert any('Minimum steel is not being checked' in s for s in sentences)
def test_concrete_grid_has_release_and_inplace_strength_fields():
blocks, _problems = pc.page({})
keys = set(_field_keys(blocks))
assert 'fci_psi' in keys
assert 'fc_psi' in keys
assert 'conc_eci_psi' in keys
assert 'conc_ec_psi' in keys
def test_ksi_reconciliation_hint_on_psi_keys():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') == 'field' and b['key'] in ('fc_psi', 'fci_psi'):
assert 'ksi' in b['hint'].lower()
def test_required_fields_match_panelanalysisform_where_it_exists():
from vibe import panelanalysisform as pf
blocks, _problems = pc.page({})
required_here = dict((b['key'], b['required'])
for b in _walk(blocks) if b.get('kind') == 'field')
for f in pf.FIELDS:
key = f['key']
if key in required_here:
assert required_here[key] == f['required'], (
'required flag mismatch for {0!r}'.format(key))
def test_panel_header_kv_shown_only_when_reads_present():
blocks_no_reads, _ = pc.page({}, {})
kvs_no_reads = [b for b in blocks_no_reads if b.get('kind') == 'kv']
blocks_with_reads, _ = pc.page({}, {'mark': 'sample_mark'})
kvs_with_reads = [b for b in blocks_with_reads if b.get('kind') == 'kv']
assert len(kvs_with_reads) >= len(kvs_no_reads) + 1
def test_page_import_survives_a_broken_panelanalysisform(monkeypatch):
"""Simulates the sibling agent's file being mid-edit / unimportable.
`_existing_keys()` is the one function that reaches for
`panelanalysisform`; monkeypatching it directly to behave the way it
would if the import raised (returning `None`) exercises the same
"could not judge" path `page()` must degrade through, without depending
on the interpreter's module-cache mechanics for a package (`vibe`)
that other tests in this same process have already imported
`panelanalysisform` onto as an attribute.
"""
monkeypatch.setattr(pc, '_existing_keys', lambda: None)
blocks, problems = pc.page({})
assert isinstance(blocks, list) and blocks
assert any('could not read panelanalysisform' in p for p in problems)
def test_choice_field_has_choices():
blocks, _problems = pc.page({})
for b in _walk(blocks):
if b.get('kind') == 'field' and b['key'] == 'concrete_type':
assert b['editor'] == 'choice'
assert 'Normalweight' in b['choices']