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.
198 lines
8.4 KiB
Python
198 lines
8.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""The handling stress limit, from the window down to the job file.
|
|
|
|
`handling.tension_limit_sqrt_fc` multiplies sqrt(f'c) to give the tensile
|
|
allowable in psi, and there are two defensible answers: 5.0 -- the modulus of
|
|
rupture over a 1.5 safety factor, "no discernible cracking" -- and 7.5, the
|
|
rupture modulus itself, first cracking with no margin. They are a factor of
|
|
1.5 apart on EVERY stress verdict, which is the largest single source of
|
|
disagreement between this tool and a commercial check of the same panel.
|
|
|
|
The owner would not pick one for everybody, 2026-08-30:
|
|
|
|
"应力极限写入 projectconfig 里面的 lifting&handling 板块里面去,让用户自己
|
|
填写。I can see it both ways."
|
|
|
|
So it moved OUT of `configform.NOT_SHOWN` and INTO the lifting & handling
|
|
module as a number box. What is tested here is the half that makes that
|
|
honest: the key a person types into is the key `liftexport` reads, and the
|
|
value reaches every stage of the job file panellift is handed. A field that
|
|
writes a key nothing reads is the drift `tools/knowledge_drift.py` exists to
|
|
catch, and it has caught it before.
|
|
"""
|
|
from vibe import configform, configmodel, knowledge, liftexport
|
|
|
|
from test_liftexport import FACE_AND_EDGE_LIFTERS, make_job, make_know
|
|
|
|
|
|
KEY = 'handling.tension_limit_sqrt_fc'
|
|
|
|
|
|
# ------------------------------------------------------- it is in the window
|
|
|
|
def test_the_stress_limit_is_a_field_in_lifting_and_handling():
|
|
"""The owner's ruling, as a placement. It used to be in NOT_SHOWN with
|
|
the reason 'a stress limit is argued for once and then left alone' --
|
|
which was an argument for having a doctrine, and he declined to have
|
|
one."""
|
|
assert configform.MODULE_OF[KEY] == configform.LIFTING
|
|
assert KEY not in configform.NOT_SHOWN
|
|
|
|
|
|
def test_it_is_a_box_you_type_a_number_into():
|
|
field = [f for f in configform.settings_fields() if f['key'] == KEY][0]
|
|
assert field['kind'] == 'number'
|
|
ok, value, _why = configform.parse(field, '7.5')
|
|
assert ok and value == 7.5
|
|
|
|
|
|
def test_the_row_note_puts_both_reference_values_in_front_of_the_typist():
|
|
"""A box that says 'stress limit' and nothing else asks a person to
|
|
remember which two numbers are argued about. The whole reason this is a
|
|
field rather than a doctrine is that the choice is the engineer's, so
|
|
the window has to show what is being chosen between."""
|
|
field = [f for f in configform.settings_fields() if f['key'] == KEY][0]
|
|
note = field['note']
|
|
assert '5.0' in note and '7.5' in note
|
|
assert "sqrt(f'c)" in note # what the number multiplies
|
|
assert 'psi' in note
|
|
assert 'cracking' in note # and what each end of it means
|
|
|
|
|
|
def test_the_full_view_says_where_it_is_edited_now():
|
|
"""`configpage` reads `placement()`, so the two halves of the answer
|
|
come from one table. Before the move this key was absent from it and
|
|
the page said 'the file'."""
|
|
assert configform.placement()[KEY] == 'Lifting and handling'
|
|
|
|
|
|
def test_nothing_fell_through_the_move():
|
|
"""The guard that makes the not-shown pool honest: every editable
|
|
setting is in a module or has a written reason for not being."""
|
|
assert configform.unmapped() == []
|
|
assert len(configform.not_shown_rows()) >= 8
|
|
assert KEY not in [key for _label, key, _why
|
|
in configform.not_shown_rows()]
|
|
|
|
|
|
# ------------------------------------------------------------ what it refuses
|
|
|
|
def test_a_zero_or_negative_limit_is_refused():
|
|
"""The only refusal, and it is not an opinion: a zero allowable fails
|
|
every piece at any stress, and a negative one inverts the comparison so
|
|
that every piece passes."""
|
|
assert configmodel.validate(KEY, 0)
|
|
assert configmodel.validate(KEY, -5.0)
|
|
assert 'greater than zero' in ' '.join(configmodel.validate(KEY, 0))
|
|
|
|
|
|
def test_both_schools_are_accepted_without_argument():
|
|
"""5.0 and 7.5 are each somebody's defensible position. The validator
|
|
does not get to have a doctrine the owner declined to have."""
|
|
assert configmodel.validate(KEY, 5.0) == []
|
|
assert configmodel.validate(KEY, 7.5) == []
|
|
|
|
|
|
def test_the_extremes_are_warned_about_and_still_allowed():
|
|
"""Above 7.5 and below 3 are worth saying something about; neither is
|
|
refused, because a refusal here would be the tool taking the position
|
|
the owner left to the engineer."""
|
|
assert configmodel.validate(KEY, 9.0) == []
|
|
assert configmodel.validate(KEY, 2.0) == []
|
|
high = ' '.join(configmodel.warnings(KEY, 9.0))
|
|
low = ' '.join(configmodel.warnings(KEY, 2.0))
|
|
assert 'rupture' in high and high
|
|
assert 'conservative' in low and low
|
|
|
|
|
|
def test_seven_point_five_says_what_it_is():
|
|
said = ' '.join(configmodel.warnings(KEY, 7.5))
|
|
assert 'first cracking' in said and 'no margin' in said
|
|
|
|
|
|
def test_the_ordinary_values_say_nothing():
|
|
"""A warning on every value is a warning nobody reads."""
|
|
assert configmodel.warnings(KEY, 5.0) == []
|
|
assert configmodel.warnings(KEY, 6.0) == []
|
|
assert configmodel.warnings(KEY, 3.5) == []
|
|
|
|
|
|
# ------------------------------------------------- it reaches the calculation
|
|
|
|
def test_the_default_is_still_five():
|
|
"""Byte-identity when nobody sets it. Every job configured today has no
|
|
override, and this is the number they have all been running."""
|
|
assert knowledge.get(knowledge.DEFAULTS, KEY) == 5.0
|
|
|
|
|
|
def test_the_job_layer_value_lands_on_every_stage_of_the_job_file():
|
|
"""The chain, as one assertion: knowledge -> default_stages -> the
|
|
stages panellift is handed. This is the link that makes the field real
|
|
rather than a knob nothing turns."""
|
|
know = make_know()
|
|
know['handling']['tension_limit_sqrt_fc'] = 7.5
|
|
stages, _rows, _problems = liftexport.default_stages(
|
|
make_job(FACE_AND_EDGE_LIFTERS), know)
|
|
assert stages
|
|
assert all(s['tension_limit_sqrt_fc'] == 7.5 for s in stages)
|
|
|
|
|
|
def test_an_unset_limit_writes_the_five_it_always_wrote():
|
|
stages, _rows, _problems = liftexport.default_stages(
|
|
make_job(FACE_AND_EDGE_LIFTERS), make_know())
|
|
assert all(s['tension_limit_sqrt_fc'] == 5.0 for s in stages)
|
|
assert all(s['tension_limit_source'] == 'job' for s in stages)
|
|
|
|
|
|
def test_a_stage_that_states_its_own_limit_is_marked_as_the_override():
|
|
"""A `handling.stages` entry beats the job-wide field. The engineer who
|
|
typed 5.0 into Project Config and reads 7.5 on the report has to be able
|
|
to see why on the page rather than by opening the YAML."""
|
|
know = make_know()
|
|
know['handling']['tension_limit_sqrt_fc'] = 5.0
|
|
know['handling']['stages'][0]['tension_limit_sqrt_fc'] = 7.5
|
|
stages, _rows, _problems = liftexport.default_stages(
|
|
make_job(FACE_AND_EDGE_LIFTERS), know)
|
|
assert stages[0]['tension_limit_sqrt_fc'] == 7.5
|
|
assert stages[0]['tension_limit_source'] == 'stage'
|
|
assert stages[1]['tension_limit_sqrt_fc'] == 5.0
|
|
assert stages[1]['tension_limit_source'] == 'job'
|
|
|
|
|
|
def test_the_provenance_words_are_the_ones_panellift_reads():
|
|
"""Written on one side, refused on the other if they disagree. panellift
|
|
raises on an unknown word rather than printing it, so the two lists have
|
|
to be the same list."""
|
|
stages, _rows, _problems = liftexport.default_stages(
|
|
make_job(FACE_AND_EDGE_LIFTERS), make_know())
|
|
assert set(s['tension_limit_source'] for s in stages) <= \
|
|
set(['job', 'stage', 'default'])
|
|
|
|
|
|
# ------------------------------------------------- and the warning is spoken
|
|
|
|
def _button_source():
|
|
import io
|
|
from test_configform import SCRIPT
|
|
return io.open(SCRIPT, encoding='utf-8').read()
|
|
|
|
|
|
def test_the_window_actually_calls_warnings():
|
|
"""`configmodel.warnings()` had no caller outside the test suite -- every
|
|
sentence in it was written and never shown. This setting is the reason it
|
|
needs one: 5.0 and 7.5 are both defensible so neither may be refused, and
|
|
the only thing left to do is say what the number means before it is
|
|
written."""
|
|
source = _button_source()
|
|
assert 'configmodel.warnings(' in source
|
|
|
|
|
|
def test_a_warning_is_not_dressed_as_a_refusal():
|
|
"""The two read the same on screen and mean opposite things. The window
|
|
paints the line so it gets read and words it so it is not mistaken for a
|
|
rejection, and it survives the window closing into the summary."""
|
|
source = _button_source()
|
|
assert 'ALLOWED, WITH A NOTE' in source
|
|
assert 'Allowed, with a note' in source
|
|
assert 'self.cautioned' in source
|