# -*- 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
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 "
" too (see that module's own note beside `_sec()`). assert len(re.findall(r'
', html)) == 12 assert html.count(' 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*]*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'Bearingx \(ft\).*?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 '

Blockouts

' in html