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.
240 lines
8.9 KiB
Python
240 lines
8.9 KiB
Python
# -*- 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
|