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.
240 lines
10 KiB
Python
240 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The tensile limit is a job decision, and the report has to say whose.
|
|
|
|
`tension_limit_sqrt_fc` multiplies sqrt(f'c) to give the allowable tension in
|
|
psi. 5.0 is the modulus of rupture over a 1.5 safety factor -- "no
|
|
discernible cracking"; 7.5 is the rupture modulus itself, first cracking with
|
|
no margin. The two are a factor of 1.5 apart on EVERY stress verdict, which
|
|
is more than any refinement of the analysis moves, and a commercial check of
|
|
one of these panels ran at 7.5 while this defaulted to 5.0 -- that difference
|
|
alone accounted for most of the disagreement between the two.
|
|
|
|
The owner declined to settle it, 2026-08-30:
|
|
|
|
"应力极限写入 projectconfig 里面的 lifting&handling 板块里面去,让用户自己
|
|
填写。I can see it both ways."
|
|
|
|
So the engineer fills it in per job, and these tests hold the two halves of
|
|
that: the number the job states is the number the check uses, and the page
|
|
says which number that was and who chose it. A report that states 666 psi
|
|
with nothing else on it cannot be told apart from the same panel at 444 psi
|
|
by anybody reading it later, which is the whole failure being prevented.
|
|
"""
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from panellift import cli, layout, report
|
|
from panellift.model import Stage
|
|
from conftest import LENGTH_FT, THICKNESS_IN, WIDTH_FT
|
|
from panellift import pci
|
|
|
|
|
|
def job(limit=None, source=None, stage_extra=None):
|
|
"""The ASPIRE panel as the exporter writes it, at whatever limit.
|
|
|
|
`limit=None` leaves the key out entirely -- a job file written before
|
|
anybody could set this, which must behave exactly as it always did.
|
|
"""
|
|
left, right = pci.optimal_two_line_supports(LENGTH_FT)
|
|
stage = {'name': 'stripping',
|
|
'multiplier': pci.stripping_multiplier('flat', 'smooth'),
|
|
'fc_psi': 4000.0}
|
|
if limit is not None:
|
|
stage['tension_limit_sqrt_fc'] = limit
|
|
if source is not None:
|
|
stage['tension_limit_source'] = source
|
|
stage.update(stage_extra or {})
|
|
return {
|
|
'schema': 'panellift/1',
|
|
'panel': {
|
|
'mark': 'ASPIRE-1',
|
|
'length_ft': LENGTH_FT,
|
|
'width_ft': WIDTH_FT,
|
|
'thickness_in': THICKNESS_IN,
|
|
'unit_weight_pcf': 150.0,
|
|
'fci_psi': 4000.0,
|
|
'fc_psi': 5000.0,
|
|
'lifters': [
|
|
{'mark': 'L1', 'family': 'TEST_LIFTER', 'x': left,
|
|
'y': 2.5, 'capacity_lb': 12000.0},
|
|
{'mark': 'L2', 'family': 'TEST_LIFTER', 'x': left,
|
|
'y': 7.5, 'capacity_lb': 12000.0},
|
|
{'mark': 'L3', 'family': 'TEST_LIFTER', 'x': right,
|
|
'y': 2.5, 'capacity_lb': 12000.0},
|
|
{'mark': 'L4', 'family': 'TEST_LIFTER', 'x': right,
|
|
'y': 7.5, 'capacity_lb': 12000.0},
|
|
],
|
|
},
|
|
'rigging': {'mode': 'rolling', 'sling_angle_deg': 90.0},
|
|
'stages': [stage],
|
|
}
|
|
|
|
|
|
def analysed(**kw):
|
|
"""(analysis, rigging, findings) for the one stage of that job."""
|
|
_panel, _rigging, cases = cli.analyse_job(job(**kw))
|
|
return cases[0]
|
|
|
|
|
|
def rendered(**kw):
|
|
"""One stage of one piece, all the way to HTML."""
|
|
return report.build_many([analysed(**kw)],
|
|
recommendation=layout.not_searched())
|
|
|
|
|
|
# ------------------------------------------------- the value reaches the sum
|
|
|
|
def test_the_job_files_limit_is_the_limit_the_check_uses():
|
|
"""The whole chain in one assertion: knowledge -> job file -> Stage ->
|
|
allowable. A field that writes a key nothing reads is the drift this
|
|
repository keeps finding, so the number has to move the arithmetic."""
|
|
five = analysed(limit=5.0)[0].stage
|
|
seven = analysed(limit=7.5)[0].stage
|
|
assert five.allowable_tension_psi == pytest.approx(5.0 * 4000.0 ** 0.5)
|
|
assert seven.allowable_tension_psi == pytest.approx(7.5 * 4000.0 ** 0.5)
|
|
# 1.5x, exactly, which is the entire argument.
|
|
assert (seven.allowable_tension_psi /
|
|
five.allowable_tension_psi) == pytest.approx(1.5)
|
|
|
|
|
|
def test_the_limit_changes_the_number_printed_on_a_real_report():
|
|
"""Not just the model object -- the rendered page. Two reports of the
|
|
same piece under the two schools must not be able to look alike."""
|
|
five = rendered(limit=5.0)
|
|
seven = rendered(limit=7.5)
|
|
assert '316 psi' in five and '474 psi' in seven
|
|
assert '474 psi' not in five and '316 psi' not in seven
|
|
|
|
|
|
def test_a_permissive_limit_can_turn_a_fail_into_a_pass():
|
|
"""Why this is the largest single source of disagreement: the verdict
|
|
itself moves, on the same panel, with the same geometry and loads."""
|
|
def verdict(limit):
|
|
# Same panel, same picks, same loads. Only f'ci is dropped to 1500
|
|
# psi, which puts the stripping stress between the two allowables:
|
|
# 5*sqrt(1500) = 194 psi and 7.5*sqrt(1500) = 290 psi.
|
|
found = [f for f in analysed(limit=limit,
|
|
stage_extra={'fc_psi': 1500.0})[2]
|
|
if f.code == 'CRACK']
|
|
return found[0].severity if found else None
|
|
|
|
assert verdict(5.0) == 'FAIL'
|
|
assert verdict(7.5) in ('OK', 'WARN')
|
|
|
|
|
|
# ----------------------------------------------------- unset is unchanged
|
|
|
|
def test_a_job_file_with_no_limit_still_gets_five():
|
|
"""Byte-identity for everything already on disk. Every job file written
|
|
before this was settable leaves the key out, and absent has to mean the
|
|
5.0 it has always meant -- not "ask", not "inherit something"."""
|
|
assert analysed()[0].stage.tension_limit_sqrt_fc == 5.0
|
|
assert Stage(name='s', multiplier=1.0, fc_psi=4000.0) \
|
|
.tension_limit_sqrt_fc == 5.0
|
|
|
|
|
|
def test_absent_and_explicit_five_compute_identically():
|
|
"""The default is the same code path as a stated 5.0, down to the
|
|
findings text -- apart from the one word saying who chose it."""
|
|
absent, stated = analysed(), analysed(limit=5.0)
|
|
a = [(f.severity, f.code) for f in absent[2]]
|
|
b = [(f.severity, f.code) for f in stated[2]]
|
|
assert a == b
|
|
assert (absent[0].stage.allowable_tension_psi ==
|
|
stated[0].stage.allowable_tension_psi)
|
|
|
|
|
|
# --------------------------------------------------------- who chose it
|
|
|
|
def test_provenance_is_derived_when_the_file_does_not_say():
|
|
"""A stage that states the limit is a stage somebody set; one that does
|
|
not is running panellift's own number. Neither needs a second key."""
|
|
assert analysed()[0].stage.tension_limit_source == 'default'
|
|
assert analysed(limit=7.5)[0].stage.tension_limit_source == 'job'
|
|
|
|
|
|
def test_the_exporter_can_say_a_stage_overrode_the_job():
|
|
"""The one thing this side cannot work out for itself: a handling.stages
|
|
entry that beat the job-wide Project Config field. Without it, the
|
|
engineer who typed 5.0 into that field and got a 7.5 report has nothing
|
|
on the page that explains the difference."""
|
|
stage = analysed(limit=7.5, source='stage')[0].stage
|
|
assert stage.tension_limit_source == 'stage'
|
|
assert 'stage' in stage.tension_basis
|
|
|
|
|
|
def test_an_unknown_provenance_word_is_refused_not_displayed():
|
|
"""A label that can be quietly wrong is worse than no label: 'jobb' read
|
|
as 'job' would print a confident sentence about the wrong layer."""
|
|
with pytest.raises(cli.JobError) as exc:
|
|
cli.analyse_job(job(limit=7.5, source='jobb'))
|
|
assert 'tension_limit_source' in str(exc.value)
|
|
|
|
|
|
def test_the_basis_string_is_ascii():
|
|
"""Findings are printed to whatever console launched the run. A radical
|
|
sign there is a crash on a cp1252 code page, not a nicer report."""
|
|
stage = Stage(name='s', multiplier=1.0, fc_psi=4000.0)
|
|
stage.tension_basis.encode('ascii')
|
|
assert stage.tension_limit_text == "5*sqrt(f'c)"
|
|
assert Stage(name='s', multiplier=1.0, fc_psi=4000.0,
|
|
tension_limit_sqrt_fc=7.5).tension_limit_text == \
|
|
"7.5*sqrt(f'c)"
|
|
|
|
|
|
# ------------------------------------------------ the report cannot be mute
|
|
|
|
def test_every_finding_that_states_an_allowable_states_the_limit():
|
|
"""CRACK is the check the limit decides, and its line has to carry the
|
|
coefficient beside the psi -- 444 and 666 on the same concrete are two
|
|
doctrines, and the psi alone does not say which."""
|
|
found = [f for f in analysed(limit=7.5)[2] if f.code == 'CRACK']
|
|
assert found
|
|
assert "7.5*sqrt(f'c), job" in found[0].message
|
|
|
|
|
|
def test_the_stress_plot_title_names_the_limit_it_drew():
|
|
"""It used to say 5*sqrt(f'c) whatever the stage ran at -- the one place
|
|
a 7.5 job was labelled as a 5.0 one."""
|
|
svg = report.stress_plot(analysed(limit=7.5)[0])
|
|
assert '7.5' in svg
|
|
assert 'against 5' not in svg
|
|
|
|
|
|
def test_the_basis_list_says_where_the_coefficient_came_from():
|
|
page = rendered(limit=7.5)
|
|
assert 'as the job file states it' in page
|
|
# notate() dresses "7.5*sqrt(f'c)" up as a real radical over a real
|
|
# f-prime-sub-c, not the ASCII the underlying strings still carry.
|
|
assert '7.5' + report.RADICAL_FC in page
|
|
|
|
|
|
def test_the_default_is_named_as_a_default_on_the_page():
|
|
"""The report must not present panellift's own fallback as a decision
|
|
somebody made."""
|
|
page = rendered()
|
|
assert "panellift's own default" in page or \
|
|
"panellift's own default" in page
|
|
|
|
|
|
def test_the_stage_table_carries_the_limit_under_the_allowable():
|
|
"""Two stages of one piece may run at different limits, so it is per row
|
|
rather than a footnote."""
|
|
page = rendered(limit=7.5)
|
|
assert 'class="limbasis"' in page
|
|
# notate() renders "7.5*sqrt(f'c)" as a real radical, so the cell now
|
|
# carries nested tags -- match to the closing </span> of the limbasis
|
|
# element itself, not to the first '<' inside it.
|
|
cell = re.search(r'<span class="limbasis">(.*?)</span></td>', page)
|
|
assert cell and '7.5' in cell.group(1) and 'job' in cell.group(1)
|
|
assert report.RADICAL_FC in cell.group(1)
|
|
|
|
|
|
def test_the_stage_box_shows_the_limit_beside_the_ratio():
|
|
"""The boxes are what a reader takes in and moves on from."""
|
|
page = rendered(limit=7.5)
|
|
box = page[page.index('<div class="stagebox-detail">'):][:260]
|
|
# notate() renders "7.5*sqrt(f'c), job" as a real radical + f-prime-sub-c.
|
|
assert '7.5' + report.RADICAL_FC + ', job' in box
|