WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The checks, and that the report they feed actually builds.
|
||||
|
||||
The ASPIRE examples are re-used here as verdict tests rather than number
|
||||
tests: example 1 must come back OK and the pocketed example 2 must come back
|
||||
FAIL. A refactor that quietly turns a printed failure into a pass is the
|
||||
regression worth catching.
|
||||
"""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from panellift import checks, pci, report, statics
|
||||
from panellift.model import Blockout, Lifter, Panel, Prestress, Rigging, Stage, Strand
|
||||
|
||||
from conftest import make_panel, aspire_pockets
|
||||
|
||||
|
||||
def _analyse(panel, stage, rigging):
|
||||
return statics.analyse(panel, stage, rigging, reconcile_weight=False)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ verdicts
|
||||
|
||||
def test_solid_panel_passes_stripping(stripping, rolling):
|
||||
a = _analyse(make_panel(), stripping, rolling)
|
||||
found = checks.check_cracking(a, stripping)
|
||||
assert [f.severity for f in found] == ['OK']
|
||||
|
||||
|
||||
def test_pocketed_panel_fails_stripping(stripping, rolling):
|
||||
"""Same panel, five pockets at the pick, and the answer must flip."""
|
||||
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
|
||||
found = checks.check_cracking(a, stripping)
|
||||
assert found[0].severity == 'FAIL'
|
||||
assert 'psi' in found[0].message
|
||||
|
||||
|
||||
def test_the_governing_station_is_at_a_pocket(stripping, rolling):
|
||||
"""The worst stress must be found at the blockout, not on the grid."""
|
||||
panel = make_panel(blockouts=aspire_pockets())
|
||||
a = _analyse(panel, stripping, rolling)
|
||||
found = checks.check_cracking(a, stripping)
|
||||
edge = panel.blockouts[0]
|
||||
assert edge.x0 - 0.1 <= found[0].at_ft <= edge.x1 + 0.1
|
||||
|
||||
|
||||
# --------------------------------------------------- prestress loss note
|
||||
|
||||
PULL_LB = 0.153 * 270_000 * 0.75 # 75% pull, 1/2 in. 270k strand
|
||||
|
||||
|
||||
def _near_miss_analysis(loss_pct):
|
||||
"""A hand-built Analysis, bypassing the beam solve entirely.
|
||||
|
||||
Station and moment are chosen so the worst corner lands at 95% of the
|
||||
tensile limit -- squarely inside the WARN band rather than balanced on
|
||||
its edge -- for a 4 x 20 ft, 6 in. solid panel. loss_pct=None gives a
|
||||
panel with no prestress at all, at a moment tuned to land in the same
|
||||
band on bending alone.
|
||||
"""
|
||||
panel = Panel(mark='NM', length_ft=20.0, width_ft=4.0, thickness_in=6.0,
|
||||
fc_psi=5000.0, fci_psi=3500.0)
|
||||
if loss_pct is not None:
|
||||
# y and z at the section's own centroid, so neither eccentricity
|
||||
# term fires and the axial term alone does the work of getting this
|
||||
# close to the limit.
|
||||
strand = Strand(y=2.0, z_in=3.0, area_in2=0.153,
|
||||
initial_pull_lb=PULL_LB)
|
||||
panel.prestress = Prestress(strands=[strand], loss_pct=loss_pct)
|
||||
moment_lbft = 10_320.0
|
||||
else:
|
||||
moment_lbft = 8_061.0
|
||||
diagram = statics.Diagram(xs=[10.0], shear_lb=[0.0],
|
||||
moment_lbft=[moment_lbft])
|
||||
stage = Stage(name='test', multiplier=1.0, fc_psi=5000.0)
|
||||
analysis = statics.Analysis(panel=panel, stage=stage, rigging=Rigging(),
|
||||
rig=None, profile=None, diagram=diagram)
|
||||
return analysis, stage
|
||||
|
||||
|
||||
def test_near_miss_with_prestress_names_the_loss_assumption():
|
||||
analysis, stage = _near_miss_analysis(loss_pct=12.5)
|
||||
found = checks.check_cracking(analysis, stage)
|
||||
crack = found[0]
|
||||
assert crack.severity == 'WARN' # verdict is unchanged
|
||||
assert 'within 10% of the limit' in crack.message
|
||||
assert '12.5%' in crack.message # read from the config,
|
||||
assert '8.0%' not in crack.message # not the 8% default
|
||||
assert 'stated assumption, not a calculated one' in crack.message
|
||||
assert 'computed over time' in crack.message
|
||||
|
||||
|
||||
def test_near_miss_without_prestress_has_no_loss_note():
|
||||
"""No prestress credited, so there is no loss assumption to question."""
|
||||
analysis, stage = _near_miss_analysis(loss_pct=None)
|
||||
found = checks.check_cracking(analysis, stage)
|
||||
crack = found[0]
|
||||
assert crack.severity == 'WARN'
|
||||
assert 'within 10% of the limit' in crack.message
|
||||
assert 'loss' not in crack.message
|
||||
|
||||
|
||||
# ------------------------------------------------------------- inserts
|
||||
|
||||
def test_insert_check_defactors_the_reaction(solid_panel, stripping, rolling):
|
||||
"""PCI Table 8.3.1 note (a): the handling multiplier is not for inserts.
|
||||
|
||||
Two stages differing only in multiplier must give the same line force.
|
||||
"""
|
||||
def line_force(mult):
|
||||
stage = Stage('s', mult, 4000.0)
|
||||
a = _analyse(make_panel(), stage, rolling)
|
||||
found = [f for f in checks.check_insert_capacity(a, rolling)
|
||||
if f.code == 'INSERT']
|
||||
return found[0].message
|
||||
|
||||
assert line_force(1.0) == line_force(1.7)
|
||||
|
||||
|
||||
def test_shallow_sling_raises_the_line_force(solid_panel, stripping):
|
||||
vertical = Rigging(mode='rolling', sling_angle_deg=90.0)
|
||||
shallow = Rigging(mode='rolling', sling_angle_deg=30.0)
|
||||
a = _analyse(make_panel(), stripping, vertical)
|
||||
|
||||
v = checks.check_insert_capacity(a, vertical)
|
||||
s = checks.check_insert_capacity(a, shallow)
|
||||
assert not any(f.code == 'SLING-ANGLE' for f in v)
|
||||
assert any(f.code == 'SLING-ANGLE' for f in s)
|
||||
|
||||
|
||||
def test_unrated_insert_warns_rather_than_passing(stripping, rolling):
|
||||
panel = make_panel()
|
||||
for lifter in panel.lifters:
|
||||
lifter.capacity_lb = None
|
||||
a = _analyse(panel, stripping, rolling)
|
||||
found = checks.check_insert_capacity(a, rolling)
|
||||
assert found[0].code == 'INSERT-UNRATED'
|
||||
assert found[0].severity == 'WARN'
|
||||
|
||||
|
||||
def test_an_overloaded_insert_fails(stripping, rolling):
|
||||
panel = make_panel()
|
||||
for lifter in panel.lifters:
|
||||
lifter.capacity_lb = 100.0
|
||||
a = _analyse(panel, stripping, rolling)
|
||||
found = checks.check_insert_capacity(a, rolling)
|
||||
assert any(f.severity == 'FAIL' for f in found)
|
||||
|
||||
|
||||
# --------------------------------- owner's ruling, 2026-09-02: decomposition
|
||||
#
|
||||
# "panel lift 里面,现在如果有sling angle的话,是直接给tension放大。不要直接给
|
||||
# tension放大,把力分解成tension跟shear,并且依次检查lifter的tension和shear
|
||||
# capacity。举例来说P136 at 18.83 ft: line force 7,609 lb (service 6,589 lb /
|
||||
# sin 60°) vs 8,000 lb tension (95%),就是tension部分6589/8000,shear部分拆
|
||||
# 出来单独算,并且跟shear capacity进行对比。二者都小于1即可,不考虑interaction."
|
||||
#
|
||||
# The wrong arithmetic compared the WHOLE amplified line force (7,609 lb)
|
||||
# to the tension rating and printed 95% -- a near-WARN that isn't real. The
|
||||
# decomposition the owner asked for compares only the vertical share (the
|
||||
# service reaction itself, 6,589 lb) to tension, and separately the
|
||||
# horizontal share (6,589 * cot(60deg) = 3,804 lb) to shear.
|
||||
|
||||
def _p136_analysis(sling_angle_deg=60.0, capacity_lb=8000.0,
|
||||
shear_capacity_lb=8000.0, service=6589.0):
|
||||
lifter = Lifter(mark='P136', family='P136', x=18.83, y=0.0,
|
||||
capacity_lb=capacity_lb,
|
||||
shear_capacity_lb=shear_capacity_lb)
|
||||
line = statics.SupportLine(x=18.83, lifters=[lifter], reaction_lb=service)
|
||||
rig = statics.Rig(lines=[line], points=1, mode='rolling')
|
||||
stage = Stage('erect', 1.0, 5000.0)
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=sling_angle_deg)
|
||||
analysis = statics.Analysis(panel=make_panel(), stage=stage,
|
||||
rigging=rigging, rig=rig, profile=None,
|
||||
diagram=None)
|
||||
return analysis, rigging
|
||||
|
||||
|
||||
def test_owner_worked_example_p136_decomposes_tension_and_shear():
|
||||
"""The pinned numbers from the ruling: 82% tension (not the old 95% on
|
||||
the amplified line force), and a shear share of ~3,804 lb checked on
|
||||
its own against the shear column -- both under 1, no interaction."""
|
||||
analysis, rigging = _p136_analysis()
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
msg = found[0].message
|
||||
assert 'tension 6,589 lb vs 8,000 lb (82%)' in msg
|
||||
assert 'shear 3,804 lb vs 8,000 lb (48%)' in msg
|
||||
# The old (wrong) arithmetic's 95% verdict does not survive.
|
||||
assert '95%' not in msg
|
||||
# 82% and 48% are each under 1 -- the finding must read as OK, not the
|
||||
# WARN the old single-ratio comparison against the amplified 7,609 lb
|
||||
# line force gave.
|
||||
assert found[0].severity == 'OK'
|
||||
|
||||
|
||||
def test_vertical_rigging_degrades_the_shear_share_to_zero():
|
||||
"""theta = 90 (a vertical line, sling_angle_deg's own convention for
|
||||
'straight up') must fall back to the old plumb-vertical arithmetic
|
||||
exactly: cot(90) = 0, so the shear share is zero and only tension is a
|
||||
real question."""
|
||||
analysis, rigging = _p136_analysis(sling_angle_deg=90.0)
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
msg = found[0].message
|
||||
assert 'tension 6,589 lb vs 8,000 lb (82%)' in msg
|
||||
assert 'shear 0 lb vs 8,000 lb (0%)' in msg
|
||||
|
||||
|
||||
def test_a_missing_shear_capacity_still_judges_the_tension_half():
|
||||
"""Some lifters carry no shear rating. That is a could-not-judge on the
|
||||
shear half, named inline -- never a silent pass -- and it must not stop
|
||||
the tension half, which has everything it needs, from judging."""
|
||||
analysis, rigging = _p136_analysis(shear_capacity_lb=None)
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
msg = found[0].message
|
||||
assert 'tension 6,589 lb vs 8,000 lb (82%)' in msg
|
||||
assert 'no shear rating' in msg and 'NOT JUDGED' in msg
|
||||
codes = [f.code for f in checks.check_insert_capacity(analysis, rigging)]
|
||||
assert 'INSERT-UNRATED' in codes
|
||||
|
||||
|
||||
# --------------------- MC dict-shaped capacities: cli.py resolves, checks.py
|
||||
# judges what comes out (a number, or None + a reason)
|
||||
#
|
||||
# `cli._resolve_capacity` is what turns MC's richer `capacity_lb` /
|
||||
# `shear_capacity_lb` dicts into the (float-or-None, reason) pair
|
||||
# `check_insert_capacity` has always consumed -- these exercise the CHECK
|
||||
# side of that split: a None tension carrying a reason must surface as
|
||||
# NOT JUDGED naming it, and a real published zero on the other axis (MC's
|
||||
# LPA4T512 pin: `shear_lb: 0`) must still be judged, not treated as absent.
|
||||
|
||||
def _mc_lifter(tension_reason=None, capacity_lb=8000.0,
|
||||
shear_capacity_lb=0.0):
|
||||
"""`tension_reason` given makes tension itself unrated (None) with that
|
||||
reason -- the angle-unresolved shape. Left None, tension carries a real
|
||||
`capacity_lb` instead, so a shear-only test is not also incidentally an
|
||||
unrated-tension test."""
|
||||
return Lifter(mark='PIN', family='LPA4T512', x=18.83, y=0.0,
|
||||
capacity_lb=(None if tension_reason else capacity_lb),
|
||||
capacity_why=tension_reason,
|
||||
shear_capacity_lb=shear_capacity_lb)
|
||||
|
||||
|
||||
def test_an_unresolved_tension_dict_is_not_judged_carrying_its_reason():
|
||||
"""The angle-unresolved shape -- tension resolves to None upstream in
|
||||
cli.py, with `capacity_why` carrying the specific "sling angle not
|
||||
confirmed" sentence rather than the generic "no rating" one. The check
|
||||
must print that reason on the tension clause, not swallow it."""
|
||||
reason = ('M8 publishes LPA4T512 tension by sling angle and the plant '
|
||||
'has not confirmed which column applies')
|
||||
lifter = _mc_lifter(tension_reason=reason)
|
||||
line = statics.SupportLine(x=18.83, lifters=[lifter], reaction_lb=860.0)
|
||||
rig = statics.Rig(lines=[line], points=1, mode='rolling')
|
||||
stage = Stage('erect', 1.0, 5000.0)
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=60.0)
|
||||
analysis = statics.Analysis(panel=make_panel(), stage=stage,
|
||||
rigging=rigging, rig=rig, profile=None,
|
||||
diagram=None)
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
msg = found[0].message
|
||||
assert 'no tension rating' in msg and 'NOT JUDGED' in msg
|
||||
assert reason in msg
|
||||
|
||||
|
||||
def test_a_published_zero_shear_capacity_is_judged_not_skipped():
|
||||
"""MC's LPA4T512 publishes `shear_lb: 0` -- a real rating, not a
|
||||
could-not-judge -- so a real shear demand against it must come back a
|
||||
genuine (very bad) ratio, never an INSERT-UNRATED could-not-judge and
|
||||
never a ZeroDivisionError crash."""
|
||||
lifter = _mc_lifter(shear_capacity_lb=0.0)
|
||||
line = statics.SupportLine(x=18.83, lifters=[lifter], reaction_lb=860.0)
|
||||
rig = statics.Rig(lines=[line], points=1, mode='rolling')
|
||||
stage = Stage('erect', 1.0, 5000.0)
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=60.0) # nonzero shear
|
||||
analysis = statics.Analysis(panel=make_panel(), stage=stage,
|
||||
rigging=rigging, rig=rig, profile=None,
|
||||
diagram=None)
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
msg = found[0].message
|
||||
assert 'no shear rating' not in msg # judged, not could-not-judge
|
||||
assert 'vs 0 lb' in msg
|
||||
assert found[0].severity == 'FAIL' # any real demand over 0 fails
|
||||
codes = [f.code for f in checks.check_insert_capacity(analysis, rigging)]
|
||||
assert 'INSERT-UNRATED' not in codes
|
||||
|
||||
|
||||
def test_a_published_zero_shear_capacity_with_no_shear_demand_is_ok():
|
||||
"""0/0 -- a vertical sling puts no shear share on this axis at all, and
|
||||
an axis with no capacity that also carries no demand is OK, not a
|
||||
fabricated FAIL."""
|
||||
lifter = _mc_lifter(shear_capacity_lb=0.0)
|
||||
line = statics.SupportLine(x=18.83, lifters=[lifter], reaction_lb=860.0)
|
||||
rig = statics.Rig(lines=[line], points=1, mode='rolling')
|
||||
stage = Stage('erect', 1.0, 5000.0)
|
||||
rigging = Rigging(mode='rolling', sling_angle_deg=90.0) # cot(90) = 0
|
||||
analysis = statics.Analysis(panel=make_panel(), stage=stage,
|
||||
rigging=rigging, rig=rig, profile=None,
|
||||
diagram=None)
|
||||
found = [f for f in checks.check_insert_capacity(analysis, rigging)
|
||||
if f.code == 'INSERT']
|
||||
assert found
|
||||
assert 'shear 0 lb vs 0 lb (0%)' in found[0].message
|
||||
|
||||
|
||||
# -------------------------------------------------------- aspect ratio
|
||||
|
||||
@pytest.mark.parametrize('width,expected', [(4.0, 'OK'), (20.0, 'WARN'),
|
||||
(28.0, 'FAIL')])
|
||||
def test_near_square_panels_are_refused(width, expected):
|
||||
panel = make_panel()
|
||||
panel.width_ft = width
|
||||
assert checks.check_aspect_ratio(panel)[0].severity == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('width', [4.0, 20.0, 28.0])
|
||||
def test_turning_a_piece_does_not_change_whether_it_is_square(width):
|
||||
"""transpose() stands a horizontal panel up by swapping length and width.
|
||||
|
||||
A 26.8 x 11.5 panel is 0.43 either way round. When this check read
|
||||
width/length it came back 2.33 turned, failed, and reported the piece as
|
||||
"close to square" -- which is how all 184 horizontal panels in a real
|
||||
model failed erection and no genuine overstress could be seen behind it.
|
||||
"""
|
||||
flat = make_panel()
|
||||
flat.width_ft = width
|
||||
stood_up = make_panel()
|
||||
stood_up.length_ft, stood_up.width_ft = width, flat.length_ft
|
||||
assert (checks.check_aspect_ratio(flat)[0].severity ==
|
||||
checks.check_aspect_ratio(stood_up)[0].severity)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- dunnage
|
||||
|
||||
def _dunnage_stage(supports):
|
||||
return Stage(name='transportation', multiplier=1.4, fc_psi=5000.0,
|
||||
pci_case='transportation', supports=supports)
|
||||
|
||||
|
||||
def test_support_under_a_big_opening_is_named(rolling):
|
||||
"""One real piece: the plant's fraction-of-span rule put a support at
|
||||
5.23 ft on a panel whose opening ran 3.23 to 12.52 ft -- a support line
|
||||
under a hole. Here an 8 ft wide, full-depth opening removes 80% of a
|
||||
10 ft panel's width at x=5, which is well past 'most of the width'."""
|
||||
panel = make_panel()
|
||||
panel.blockouts = [Blockout(name='DOOR', x0=3.0, y0=0.0,
|
||||
length=4.0, width=8.0)]
|
||||
stage = _dunnage_stage([5.0, 25.0])
|
||||
a = statics.analyse(panel, stage, rolling, reconcile_weight=False)
|
||||
found = checks.check_dunnage_position(a)
|
||||
|
||||
assert len(found) == 1 # only the 5.0 ft support
|
||||
f = found[0]
|
||||
assert f.severity == 'WARN'
|
||||
assert f.code == 'DUNNAGE-OPENING'
|
||||
assert f.at_ft == 5.0
|
||||
assert '5.00 ft' in f.message
|
||||
assert 'DOOR' in f.message
|
||||
assert '3.00 to 7.00 ft' in f.message
|
||||
assert '80%' in f.message
|
||||
|
||||
|
||||
def test_support_clear_of_any_opening_is_not_flagged(rolling):
|
||||
panel = make_panel() # no blockouts at all
|
||||
stage = _dunnage_stage([5.0, 25.0])
|
||||
a = statics.analyse(panel, stage, rolling, reconcile_weight=False)
|
||||
assert checks.check_dunnage_position(a) == []
|
||||
|
||||
|
||||
def test_a_small_opening_under_a_support_does_not_trigger(rolling):
|
||||
"""A opening that leaves most of the width intact is a different case
|
||||
from a support bearing mostly on a hole -- below the fraction this draws
|
||||
the line at, it is left alone."""
|
||||
panel = make_panel()
|
||||
panel.blockouts = [Blockout(name='SMALL', x0=3.0, y0=0.0,
|
||||
length=4.0, width=2.5)] # 25% of 10 ft
|
||||
stage = _dunnage_stage([5.0, 25.0])
|
||||
a = statics.analyse(panel, stage, rolling, reconcile_weight=False)
|
||||
assert checks.check_dunnage_position(a) == []
|
||||
|
||||
|
||||
def test_dunnage_check_does_not_run_on_a_lifted_stage(stripping, rolling):
|
||||
"""supports is None on a normal pick -- nothing to ask voids_at about."""
|
||||
a = statics.analyse(make_panel(), stripping, rolling,
|
||||
reconcile_weight=False)
|
||||
assert checks.check_dunnage_position(a) == []
|
||||
|
||||
|
||||
def test_run_all_includes_the_dunnage_check_on_a_supported_stage(rolling):
|
||||
panel = make_panel()
|
||||
panel.blockouts = [Blockout(name='DOOR', x0=3.0, y0=0.0,
|
||||
length=4.0, width=8.0)]
|
||||
stage = _dunnage_stage([5.0, 25.0])
|
||||
a = statics.analyse(panel, stage, rolling, reconcile_weight=False)
|
||||
found = checks.run_all(a, rolling)
|
||||
assert any(f.code == 'DUNNAGE-OPENING' for f in found)
|
||||
|
||||
|
||||
# -------------------------------------------------------------- report
|
||||
|
||||
def test_report_builds_and_is_self_contained(stripping, rolling):
|
||||
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
|
||||
found = checks.run_all(a, rolling)
|
||||
page = report.build(a, rolling, found)
|
||||
|
||||
assert page.count('<svg') == page.count('</svg>') == 5
|
||||
assert '</table>' in page
|
||||
# A calc report has to open from disk on a machine with no network.
|
||||
assert 'http://' not in page and 'https://' not in page
|
||||
assert 'ASPIRE-1' in page or 'ASPIRE' in page
|
||||
|
||||
|
||||
def test_report_states_the_verdict(stripping, rolling):
|
||||
a = _analyse(make_panel(blockouts=aspire_pockets()), stripping, rolling)
|
||||
found = checks.run_all(a, rolling)
|
||||
assert checks.worst_severity(found) == 'FAIL'
|
||||
assert 'not satisfied' in report.build(a, rolling, found)
|
||||
|
||||
|
||||
def test_transportation_report_says_it_was_checked_as_a_lift(stripping,
|
||||
rolling):
|
||||
"""Transportation sits on dunnage, not the inserts. Until the support
|
||||
model exists, the report must say which model produced the numbers."""
|
||||
strip = _analyse(make_panel(), stripping, rolling)
|
||||
strip_page = report.build(strip, rolling,
|
||||
checks.run_all(strip, rolling),
|
||||
recommendation=False)
|
||||
assert 'dunnage' not in strip_page
|
||||
|
||||
haul = Stage(name='haul', multiplier=1.4, fc_psi=5000.0,
|
||||
pci_case='transportation')
|
||||
haul_a = _analyse(make_panel(), haul, rolling)
|
||||
haul_page = report.build(haul_a, rolling,
|
||||
checks.run_all(haul_a, rolling),
|
||||
recommendation=False)
|
||||
assert 'dunnage' in haul_page
|
||||
assert 'checked as a <b>lift</b>' in haul_page
|
||||
|
||||
|
||||
def test_transportation_on_dunnage_reports_supports_not_a_pick(rolling):
|
||||
panel = make_panel()
|
||||
haul = Stage(name='transportation', multiplier=1.4, fc_psi=5000.0,
|
||||
pci_case='transportation',
|
||||
supports=[0.2 * panel.length_ft, 0.8 * panel.length_ft])
|
||||
a = _analyse(panel, haul, rolling)
|
||||
page = report.build(a, rolling, checks.run_all(a, rolling),
|
||||
recommendation=False)
|
||||
assert 'dunnage line' in page
|
||||
assert 'checked as a <b>lift</b>' not in page
|
||||
assert 'no insert check applies' in page
|
||||
|
||||
|
||||
# ------------------------------------------------------ demo-safe: no CJK
|
||||
#
|
||||
# Owner, 2026-09-01: no Chinese anywhere in a report or a UI surface, even
|
||||
# when quoting him -- he is demoing this to colleagues before the weekend.
|
||||
# The owner's own rulings behind these findings (`capacity.py`'s M/phi-Mn
|
||||
# ruling, `sandwich.py`'s "not conservative" ruling, both quoted again in
|
||||
# `checks.py` and `cli.py`) now live in `#` comments only, never in a
|
||||
# `Finding.message`. This renders the real HTML page -- `report.build`,
|
||||
# the surface a colleague actually sees -- off a pocketed panel forced
|
||||
# into a FAIL, so the capacity and cracking findings are both live.
|
||||
def test_the_rendered_findings_page_carries_no_chinese_text_or_html(
|
||||
stripping, rolling):
|
||||
panel = make_panel(blockouts=aspire_pockets())
|
||||
a = _analyse(panel, stripping, rolling)
|
||||
findings = checks.run_all(a, rolling)
|
||||
assert any(f.severity == 'FAIL' for f in findings) # the FAIL path ran
|
||||
cjk = re.compile(u'[一-鿿]')
|
||||
for f in findings:
|
||||
assert not cjk.search(f.message), f.message
|
||||
page = report.build(a, rolling, findings, recommendation=False)
|
||||
assert not cjk.search(page), page
|
||||
Reference in New Issue
Block a user