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,338 @@
|
||||
# -*- 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']
|
||||
Reference in New Issue
Block a user