25592fa6c2
ci / test (push) Successful in 6m14s
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.
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""`tools/panelanalysis.py::build_analysis_page` -- the READ-ONLY results
|
|
page for the `panelanalysis/1` schema (2026-09, results-page redesign).
|
|
|
|
Guards the redesign's own hard rules, on two decks:
|
|
|
|
MINIMAL `schema.new_deck()` as-is -- two dunnage supports, two
|
|
straight strands, no in-place restraints/wind at all (the
|
|
Stage-0 default). Exercises the "not judged" path for
|
|
in-place strength/service.
|
|
IN-PLACE the same deck with lateral restraints and a manual wind zone
|
|
added, so the in-place strength/service models actually
|
|
solve and their own SVG envelope diagrams
|
|
(`panelanalysis._inplace_diagram`) render.
|
|
|
|
This does not repeat `test_panelanalysis_schema.py`'s coverage of the
|
|
engine/schema layer -- it only checks what the WRITTEN PAGE looks like:
|
|
- never an `<input>` or `<select>` (the input side lives in the 前端 now)
|
|
- never a DECK / NOT READ / COMPUTED badge (`CHIPS`' own vocabulary)
|
|
- the Summary card is present (`class="verdict"` + `class="stageboxes"`)
|
|
- no decimal-feet number anywhere (every length is feet-inches)
|
|
- the page is well under the ~1 MB budget and opens standalone (its own
|
|
`<style>` carries the embedded CSS -- no `<link rel="stylesheet">`)
|
|
|
|
Import pattern mirrors `test_panelanalysis_schema.py`: `tools/` on
|
|
`sys.path`, `deckcheck` imported first for its `vibe` stub side effect.
|
|
"""
|
|
import copy
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
TOOLS = os.path.abspath(os.path.join(HERE, os.pardir))
|
|
if TOOLS not in sys.path:
|
|
sys.path.insert(0, TOOLS)
|
|
|
|
import deckcheck # noqa: E402,F401 -- registers the `vibe` stub
|
|
|
|
import panelanalysis # noqa: E402
|
|
import panelanalysis_schema as schema # noqa: E402
|
|
|
|
|
|
DECIMAL_FT = re.compile(r'\d+\.\d+\s*ft\b')
|
|
|
|
|
|
def minimal_deck():
|
|
return schema.new_deck()
|
|
|
|
|
|
def in_place_deck():
|
|
"""`new_deck()`, wired for the in-place strength/service models to
|
|
solve: three lateral restraints (so `schema.restraints()` sees >= 2)
|
|
and one manual wind zone -- the same shape `WPA655.pa.json` (the deck
|
|
this redesign was built and screenshotted against) carries.
|
|
"""
|
|
deck = schema.new_deck(length_ft=37.5833, width_ft=8.375)
|
|
deck['structural_model']['restraints'] = [
|
|
{'x_ft': 2.0, 'type': 'Pin', 'gravity': True, 'thermal': False},
|
|
{'x_ft': 15.0, 'type': 'Floor tie', 'gravity': True,
|
|
'thermal': False},
|
|
{'x_ft': 22.0, 'type': 'Floor tie', 'gravity': True,
|
|
'thermal': False},
|
|
]
|
|
deck['loading']['wind'] = {
|
|
'mode': 'manual',
|
|
'zones': [{'label': 'W1', 'x0_ft': 0.0, 'x1_ft': deck[
|
|
'concrete_extents']['length_ft'], 'pressure_psf': 30.0,
|
|
'suction_psf': 35.0}],
|
|
}
|
|
return deck
|
|
|
|
|
|
def _check_common_invariants(html):
|
|
assert '<input' not in html
|
|
assert '<select' not in html
|
|
for badge in ('>DECK<', '>NOT READ<', '>COMPUTED<', '>SPEC<',
|
|
'>KNOWLEDGE<', '>PLANNED<', '>NOT COMPUTED<'):
|
|
assert badge not in html, badge
|
|
assert 'class="chip' not in html
|
|
assert '<link rel="stylesheet"' not in html
|
|
assert 'class="verdict"' in html
|
|
assert 'class="stageboxes"' in html
|
|
assert 'class="stagebox' in html
|
|
bad = DECIMAL_FT.findall(html)
|
|
assert not bad, 'decimal-feet numbers found: {0}'.format(bad)
|
|
assert len(html.encode('utf-8')) < 1024 * 1024
|
|
|
|
|
|
def test_minimal_deck_page_has_no_inputs_or_badges_and_a_summary_card():
|
|
html = panelanalysis.build_analysis_page(minimal_deck())
|
|
_check_common_invariants(html)
|
|
# Nothing wires the in-place model on this deck -- both its models are
|
|
# NOT JUDGED, named as such, never a fabricated zero.
|
|
assert 'not judged' in html.lower()
|
|
|
|
|
|
def test_in_place_deck_solves_and_draws_its_own_envelope_diagrams():
|
|
deck = in_place_deck()
|
|
html = panelanalysis.build_analysis_page(deck)
|
|
_check_common_invariants(html)
|
|
# The in-place models actually ran on this deck -- PASS/WARN/FAIL, not
|
|
# "not judged", for In-Place Strength and In-Place Service.
|
|
assert 'In-Place Strength' in html
|
|
assert 'In-Place Service' in html
|
|
# This file's own SVG diagrams for the governing in-place combination
|
|
# (`panelanalysis._inplace_diagram`) rendered -- their titles are
|
|
# unique to them (the handling stages' reused `R.diagram_plot` figures
|
|
# never say "governing combination").
|
|
assert 'governing combination' in html.lower()
|
|
|
|
|
|
def test_handling_stages_each_get_their_own_section_and_tag():
|
|
html = panelanalysis.build_analysis_page(minimal_deck())
|
|
for stage in ('Stripping', 'Transportation', 'Erection'):
|
|
assert 'Handling — {0}'.format(stage) in html or \
|
|
'Handling — {0}'.format(stage) in html
|
|
|
|
|
|
def test_page_is_deterministic_given_the_same_deck_modulo_timestamp():
|
|
"""Two builds a second apart differ only in the 'generated HH:MM' line
|
|
-- nothing else on a read-only report should vary run to run."""
|
|
deck = in_place_deck()
|
|
h1 = panelanalysis.build_analysis_page(copy.deepcopy(deck))
|
|
h2 = panelanalysis.build_analysis_page(copy.deepcopy(deck))
|
|
strip = lambda s: re.sub(r'generated \d{4}-\d\d-\d\d \d\d:\d\d',
|
|
'generated X', s)
|
|
assert strip(h1) == strip(h2)
|