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,239 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for `tools/panelanalysis.py` -- the Eriksson-shaped demo page.
|
||||
|
||||
Loaded by path, the same way `test_deckcheck.py` in this folder is: this is
|
||||
host CPython code (it imports `panellift`, dataclasses and typing syntax)
|
||||
sitting in a repository whose extension is IronPython 2.7, and it in turn
|
||||
imports `tools/deckcheck.py` for the off-Revit knowledge merge, which is
|
||||
where the `vibe` stub actually gets registered. Pre-importing `vibe.
|
||||
knowledge` here is what `test_deckcheck.py` does for the same reason: the
|
||||
stub has to exist in `sys.modules` before anything tries `import vibe.*`,
|
||||
and doing it in one place keeps every test file in this folder agreeing on
|
||||
how.
|
||||
|
||||
These are cheap smoke tests, not a full walk of every section -- the four
|
||||
things the brief asked to be inexpensive to check:
|
||||
|
||||
1. the renderer produces the section skeleton from a fixture deck
|
||||
2. populated-vs-absent field states carry the right source chip
|
||||
3. zero CJK on the page (the same guard `panellift`'s own report tests
|
||||
use, `test_checks.py`'s "demo-safe: no CJK" block)
|
||||
4. no crash on a deck with no spec -- degrades to model-only fields,
|
||||
and says so on the page rather than silently
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
TOOLS = os.path.join(ROOT, 'tools')
|
||||
PANELLIFT_SRC = os.path.join(ROOT, 'panellift', 'src')
|
||||
for p in (TOOLS, PANELLIFT_SRC):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from vibe import knowledge # noqa: E402 (registers the vibe stub)
|
||||
from vibe import plantpath # noqa: E402
|
||||
|
||||
pa = pytest.importorskip('panelanalysis')
|
||||
|
||||
CJK = re.compile(u'[一-鿿]')
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ fixture
|
||||
|
||||
def _deck(mark='TESTMK', design_number='ZZ9.0', with_prestress=True,
|
||||
with_blockout=False):
|
||||
panel = {
|
||||
'mark': mark,
|
||||
'design_number': design_number,
|
||||
'length_ft': 20.0,
|
||||
'width_ft': 8.0,
|
||||
'thickness_in': 6.0,
|
||||
'fc_psi': 6000.0,
|
||||
'fci_psi': 4000.0,
|
||||
'unit_weight_pcf': 150.0,
|
||||
'weight_lb': 10800.0,
|
||||
'finish': 'smooth',
|
||||
'product_type': 'flat',
|
||||
'blockouts': ([{'name': 'BO1', 'x0': 8.0, 'y0': 2.0, 'length': 2.0,
|
||||
'width': 2.0}] if with_blockout else []),
|
||||
'recesses': [],
|
||||
'lifters': [
|
||||
{'mark': 'L1', 'family': 'TEST_LIFTER', 'x': 3.0, 'y': 2.0,
|
||||
'capacity_lb': 8000.0},
|
||||
{'mark': 'L2', 'family': 'TEST_LIFTER', 'x': 17.0, 'y': 2.0,
|
||||
'capacity_lb': 8000.0},
|
||||
],
|
||||
}
|
||||
if with_prestress:
|
||||
panel['prestress'] = {
|
||||
'diameter_in': 0.5,
|
||||
'loss_pct': 8.0,
|
||||
'strands': [
|
||||
{'mark': 'S1', 'y': 1.0, 'z_in': 1.0, 'area_in2': 0.153,
|
||||
'initial_pull_lb': 28920.0},
|
||||
{'mark': 'S2', 'y': 4.0, 'z_in': 1.0, 'area_in2': 0.153,
|
||||
'initial_pull_lb': 28920.0},
|
||||
],
|
||||
}
|
||||
return {
|
||||
'schema': 'panellift/1',
|
||||
'panel': panel,
|
||||
'source': {
|
||||
'design': design_number.split('.')[0],
|
||||
'model': 'sample_job',
|
||||
'connectors': {
|
||||
'capacity_shear_lb': None, 'capacity_tension_lb': None,
|
||||
'count': None, 'declared_action': None, 'family': None,
|
||||
'per_row': None, 'spacing_in': None,
|
||||
'stiffness_lb_per_in': None,
|
||||
'why': 'connector recognition is not wired',
|
||||
},
|
||||
},
|
||||
'stages': [
|
||||
{'name': 'stripping', 'fc_psi': 4000.0, 'multiplier': 1.4,
|
||||
'pci_case': 'stripping', 'lifters': ['L1', 'L2']},
|
||||
{'name': 'transportation', 'fc_psi': 6000.0, 'multiplier': 1.5,
|
||||
'pci_case': 'transportation', 'supports': [3.0, 17.0]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _write_deck(tmp_path, deck, job_root_name='job_root'):
|
||||
job_root = tmp_path / job_root_name
|
||||
jobs_dir = job_root / 'sweep' / 'jobs'
|
||||
jobs_dir.mkdir(parents=True)
|
||||
deck_path = jobs_dir / '{0}.job.json'.format(deck['panel']['mark'])
|
||||
with io.open(str(deck_path), 'w', encoding='utf-8') as fh:
|
||||
json.dump(deck, fh)
|
||||
return job_root, deck_path
|
||||
|
||||
|
||||
def _write_spec(job_root, design, data):
|
||||
specs_dir = job_root / 'specs'
|
||||
specs_dir.mkdir(parents=True, exist_ok=True)
|
||||
with io.open(str(specs_dir / '{0}.yaml'.format(design)), 'w',
|
||||
encoding='utf-8') as fh:
|
||||
yaml.safe_dump(data, fh)
|
||||
|
||||
|
||||
SECTION_TITLES = [
|
||||
'General / Project Information',
|
||||
'Concrete Extents',
|
||||
'Materials',
|
||||
'Prestress / Reinforcement',
|
||||
'Structural Model (supports, bracing, connections)',
|
||||
'Settings',
|
||||
'Loading Summary',
|
||||
'Section Properties',
|
||||
'Results -- Flexure & Shear Capacity',
|
||||
'In-Place Strength / Service (not computed)',
|
||||
'Horizontal Shear Analysis (not computed)',
|
||||
'Everything this demo does not compute yet',
|
||||
]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- tests
|
||||
|
||||
def test_renders_the_section_skeleton_from_a_fixture_deck(tmp_path):
|
||||
deck = _deck()
|
||||
job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
_write_spec(job_root, 'ZZ9', {
|
||||
'design_number': 'ZZ9',
|
||||
'piece_description': 'sample panel',
|
||||
'concrete': {'fc_psi': 6000, 'fci_psi': 4000},
|
||||
'reinforcing': {'strand_groups': [
|
||||
{'qty': 2, 'dia_in': 0.5, 'grade_ksi': 270, 'it_lb': 28920}]},
|
||||
'envelope': {'length': {'min_ft': 5.0, 'max_ft': 30.0},
|
||||
'width': {'min_ft': 4.0, 'max_ft': 10.0}},
|
||||
'source': {'sheet': 'ZZ9.0', 'extractor': 'regex',
|
||||
'extracted': '2026-01-01T00:00:00'},
|
||||
})
|
||||
|
||||
html = pa.build_page(str(deck_path))
|
||||
|
||||
for title in SECTION_TITLES:
|
||||
assert '>{0}<'.format(title) in html, title
|
||||
# Twelve real <details class="section"> blocks, one per title above --
|
||||
# counted this way rather than by a bare substring count, because CSS
|
||||
# comments reused from panellift.report's stylesheet mention the literal
|
||||
# text "<details>" too (see that module's own note beside `_sec()`).
|
||||
assert len(re.findall(r'<details class="section">', html)) == 12
|
||||
assert html.count('<input') > 20
|
||||
assert 'WP242' not in html # this is the fixture, not a real job
|
||||
|
||||
|
||||
def test_populated_vs_absent_field_states(tmp_path):
|
||||
deck = _deck()
|
||||
_job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
|
||||
html = pa.build_page(str(deck_path))
|
||||
|
||||
# A value the deck actually carries: chipped DECK, not absent.
|
||||
assert re.search(r'value="TESTMK"[^>]*>\s*<span[^>]*class="chip '
|
||||
r'chip-deck"', html)
|
||||
# In-place bearing is never read off any model yet, on ANY deck -- see
|
||||
# sec_structural_model's own docstring -- so it always renders empty
|
||||
# and chipped NOT READ, whether or not the deck otherwise carries data.
|
||||
assert 'In-place bearing (not built into any stage yet)' in html
|
||||
absent_bearing = re.search(
|
||||
r'Bearing</th><th>x \(ft\)</th>.*?chip-absent', html, re.S)
|
||||
assert absent_bearing, 'expected an absent-chipped bearing input row'
|
||||
# source.connectors is null on this fixture (mirroring the real jobs) --
|
||||
# every connector field should render NOT READ.
|
||||
assert html.count('chip-absent') >= 8
|
||||
|
||||
|
||||
def test_zero_cjk(tmp_path):
|
||||
deck = _deck()
|
||||
_job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
html = pa.build_page(str(deck_path))
|
||||
m = CJK.search(html)
|
||||
assert not m, 'CJK character found: {0!r}'.format(m.group() if m else '')
|
||||
|
||||
|
||||
def test_no_spec_degrades_to_model_only_fields(tmp_path):
|
||||
"""A deck naming a design nobody wrote a spec for must not crash.
|
||||
|
||||
`load_spec` should come back with `spec=None` and a stated reason, and
|
||||
the page should say so rather than silently omitting the design-extract
|
||||
fields -- the same "could not judge is not silence" discipline the rest
|
||||
of this codebase holds to.
|
||||
"""
|
||||
deck = _deck(design_number='NOSPEC9.0')
|
||||
_job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
|
||||
html = pa.build_page(str(deck_path)) # must not raise
|
||||
|
||||
assert 'no spec file at' in html
|
||||
# The design number field still renders, sourced from the deck itself
|
||||
# rather than a spec that does not exist.
|
||||
assert 'NOSPEC9' in html
|
||||
for title in SECTION_TITLES:
|
||||
assert '>{0}<'.format(title) in html, title
|
||||
|
||||
|
||||
def test_no_prestress_deck_does_not_crash_and_says_so(tmp_path):
|
||||
deck = _deck(with_prestress=False)
|
||||
_job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
|
||||
html = pa.build_page(str(deck_path)) # must not raise
|
||||
|
||||
assert 'carries no prestress block at all' in html
|
||||
assert 'is strand-only by construction' in html
|
||||
|
||||
|
||||
def test_a_blockout_is_reflected_in_section_properties(tmp_path):
|
||||
deck = _deck(with_blockout=True)
|
||||
_job_root, deck_path = _write_deck(tmp_path, deck)
|
||||
html = pa.build_page(str(deck_path))
|
||||
assert '<h3>Blockouts</h3>' in html
|
||||
Reference in New Issue
Block a user