Files
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

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