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.
642 lines
27 KiB
Python
642 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Tests for `vibe.pa_criteria.page` -- the Design Criteria section layout.
|
||
|
||
No Revit and no WPF renderer are available here (see `conftest.py`'s
|
||
docstring for why `vibe` imports this way), so this suite cannot check that
|
||
the page LOOKS right -- nobody has run this inside Revit and this file does
|
||
not claim otherwise. What it can and does check mechanically:
|
||
|
||
* `page()` never raises, on an empty dict, a filled dict, `None` values,
|
||
`None` reads, or a `values` object that is not even a dict.
|
||
* every block returned is a well-formed member of the block vocabulary
|
||
`page()`'s docstring promises (`heading`, `columns`, `field`, `kv`,
|
||
`sentence`, `table`, `rule`, `space`) -- an unknown `kind` renders as
|
||
nothing on the real form, so a typo here would be a silently missing
|
||
block that only this test would catch.
|
||
* every `field` block carries a non-empty, plain-English `label` and a
|
||
non-empty `hint` -- the owner's whole complaint about the old form was
|
||
that a field carried only its engineering symbol.
|
||
* a field key with no backing entry in `panelanalysisform.FIELDS` still
|
||
renders (an empty editor), and is named in `problems` -- never silently
|
||
dropped.
|
||
* `values` is never mutated by a call to `page()`.
|
||
* the block list is stable: calling `page()` twice on the same input
|
||
produces the same output.
|
||
"""
|
||
import copy
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
TESTS_DIR = Path(__file__).resolve().parent
|
||
if str(TESTS_DIR) not in sys.path:
|
||
sys.path.insert(0, str(TESTS_DIR))
|
||
|
||
from vibe import pa_criteria as pc # noqa: E402
|
||
|
||
|
||
KNOWN_KINDS = set(['heading', 'columns', 'field', 'kv', 'sentence', 'table',
|
||
'lc_table', 'rule', 'space'])
|
||
KNOWN_EDITORS = set(['text', 'num', 'check', 'choice'])
|
||
|
||
|
||
def _walk(blocks):
|
||
"""Yield every block in the tree, including nested `columns` children."""
|
||
for b in blocks:
|
||
yield b
|
||
if b.get('kind') == 'columns':
|
||
for col in b.get('columns') or []:
|
||
for sub in _walk(col):
|
||
yield sub
|
||
|
||
|
||
def _assert_well_formed(blocks):
|
||
for b in blocks:
|
||
assert isinstance(b, dict), 'block is not a dict: {0!r}'.format(b)
|
||
kind = b.get('kind')
|
||
assert kind in KNOWN_KINDS, 'unknown block kind {0!r}'.format(kind)
|
||
|
||
if kind == 'heading':
|
||
assert isinstance(b['text'], str) and b['text'].strip()
|
||
assert b['level'] in (1, 2, 3)
|
||
|
||
elif kind == 'columns':
|
||
assert isinstance(b['columns'], list) and b['columns']
|
||
for col in b['columns']:
|
||
assert isinstance(col, list)
|
||
_assert_well_formed(col)
|
||
|
||
elif kind == 'field':
|
||
assert isinstance(b['key'], str) and b['key']
|
||
assert isinstance(b['label'], str) and b['label'].strip(), (
|
||
'field {0!r} has no plain-English label'.format(b['key']))
|
||
assert isinstance(b['hint'], str) and b['hint'].strip(), (
|
||
'field {0!r} has no hint'.format(b['key']))
|
||
assert b['symbol'] is None or isinstance(b['symbol'], str)
|
||
assert isinstance(b['required'], bool)
|
||
assert b['editor'] in KNOWN_EDITORS, (
|
||
'field {0!r} has unknown editor {1!r}'
|
||
.format(b['key'], b['editor']))
|
||
if b['editor'] == 'choice':
|
||
assert b['choices'], (
|
||
'choice field {0!r} has no choices'.format(b['key']))
|
||
else:
|
||
assert b['choices'] is None
|
||
|
||
elif kind == 'kv':
|
||
assert isinstance(b['rows'], list)
|
||
for row in b['rows']:
|
||
assert len(row) == 2
|
||
|
||
elif kind == 'sentence':
|
||
assert isinstance(b['text'], str) and b['text'].strip()
|
||
|
||
elif kind == 'table':
|
||
assert isinstance(b['headers'], list) and b['headers']
|
||
assert isinstance(b['rows'], list)
|
||
for row in b['rows']:
|
||
assert len(row) == len(b['headers']), (
|
||
'table row {0!r} does not match headers {1!r}'
|
||
.format(row, b['headers']))
|
||
for cell in row:
|
||
if isinstance(cell, dict):
|
||
# `_render_table` grew support for ANY editor kind in
|
||
# a cell, not just a tick box (see its own comment,
|
||
# "ANY editor kind, not just 'check'") -- that is what
|
||
# let Resistance Factors move from a squeezed
|
||
# `columns`-of-`field`s to a `table` of live `num`
|
||
# editors on 2026-09-04 (WPA655). A cell dict is no
|
||
# longer only ever a checkbox.
|
||
assert cell.get('editor') in KNOWN_EDITORS
|
||
assert cell.get('key')
|
||
else:
|
||
assert isinstance(cell, str)
|
||
|
||
elif kind == 'lc_table':
|
||
# The load-combination grid: same row/header contract as `table`,
|
||
# plus a `name` naming the table and a `factors` list the renderer
|
||
# needs to build an added row's cells. Unlike `table`, a cell may
|
||
# carry ANY editor kind -- the factors are editable now.
|
||
assert isinstance(b['name'], str) and b['name']
|
||
assert isinstance(b['headers'], list) and b['headers']
|
||
assert isinstance(b['factors'], list) and b['factors']
|
||
assert isinstance(b['rows'], list)
|
||
for row in b['rows']:
|
||
assert len(row) == len(b['headers']), (
|
||
'lc_table row {0!r} does not match headers {1!r}'
|
||
.format(row, b['headers']))
|
||
for cell in row:
|
||
if isinstance(cell, dict):
|
||
assert cell.get('editor') in KNOWN_EDITORS
|
||
assert cell.get('key')
|
||
else:
|
||
assert isinstance(cell, str)
|
||
|
||
elif kind == 'rule':
|
||
assert set(b.keys()) == set(['kind'])
|
||
|
||
elif kind == 'space':
|
||
assert isinstance(b['h'], int)
|
||
|
||
|
||
def _field_keys(blocks):
|
||
return [b['key'] for b in _walk(blocks) if b.get('kind') == 'field']
|
||
|
||
|
||
# --------------------------------------------------------------------- tests
|
||
|
||
def test_empty_values_never_raises_and_is_well_formed():
|
||
blocks, problems = pc.page({})
|
||
assert isinstance(blocks, list) and blocks
|
||
assert isinstance(problems, list)
|
||
_assert_well_formed(blocks)
|
||
|
||
|
||
def test_none_values_and_none_reads_never_raises():
|
||
blocks, problems = pc.page(None, None)
|
||
assert isinstance(blocks, list) and blocks
|
||
_assert_well_formed(blocks)
|
||
|
||
|
||
def test_non_dict_values_degrades_rather_than_raising():
|
||
blocks, problems = pc.page('not a dict')
|
||
assert isinstance(blocks, list) and blocks
|
||
_assert_well_formed(blocks)
|
||
|
||
|
||
def test_filled_values_never_raises_and_is_well_formed():
|
||
values = {
|
||
'fc_psi': '6000', 'fci_psi': '3500', 'unit_weight_pcf': '150',
|
||
'concrete_type': 'Normalweight', 'rebar_fy_ksi': '60',
|
||
'rebar_es_ksi': '29000', 'ps_fpu_ksi': '270', 'strand_ep_ksi': '28500',
|
||
'ps_loss_method': 'PCI', 'ps_loss_humidity_pct': '75',
|
||
'design_criteria.rvalue_calc_enabled': 'yes',
|
||
'design_criteria.min_reinf_checked': 'true',
|
||
}
|
||
blocks, problems = pc.page(values, {'mark': 'sample_mark',
|
||
'design_number': 'PNL1.0'})
|
||
assert isinstance(blocks, list) and blocks
|
||
_assert_well_formed(blocks)
|
||
|
||
|
||
def test_values_dict_is_never_mutated():
|
||
values = {'fc_psi': '6000'}
|
||
before = copy.deepcopy(values)
|
||
pc.page(values, {'mark': 'sample_mark'})
|
||
assert values == before
|
||
|
||
|
||
def test_reads_dict_is_never_mutated():
|
||
reads = {'mark': 'sample_mark', 'design_number': 'PNL1.0'}
|
||
before = copy.deepcopy(reads)
|
||
pc.page({}, reads)
|
||
assert reads == before
|
||
|
||
|
||
def test_block_list_is_stable_for_the_same_input():
|
||
values = {'fc_psi': '6000', 'unit_weight_pcf': '150'}
|
||
reads = {'mark': 'sample_mark'}
|
||
a, pa = pc.page(values, reads)
|
||
b, pb = pc.page(values, reads)
|
||
assert a == b
|
||
assert pa == pb
|
||
|
||
|
||
def test_missing_key_degrades_to_a_field_and_names_a_problem():
|
||
blocks, problems = pc.page({})
|
||
keys = set(_field_keys(blocks))
|
||
# A key this file invented because Eriksson's page has no home for it
|
||
# yet in panelanalysisform.FIELDS.
|
||
assert 'design_criteria.analysis_beta_d' in keys
|
||
joined = ' '.join(problems)
|
||
assert 'design_criteria.analysis_beta_d' in joined
|
||
|
||
|
||
def test_existing_form_key_is_not_reported_as_missing():
|
||
blocks, problems = pc.page({})
|
||
joined = ' '.join(problems)
|
||
# fc_psi is a real, long-standing panelanalysisform.FIELDS key.
|
||
assert "'fc_psi'" not in joined or 'fc_psi) has no backing' not in joined
|
||
for p in problems:
|
||
assert 'fc_psi)' not in p
|
||
|
||
|
||
def test_every_field_key_appears_at_most_once():
|
||
blocks, _problems = pc.page({})
|
||
keys = _field_keys(blocks)
|
||
dupes = [k for k in set(keys) if keys.count(k) > 1]
|
||
assert not dupes, 'field key(s) rendered more than once: {0}'.format(dupes)
|
||
|
||
|
||
def test_section_heading_is_present():
|
||
blocks, _problems = pc.page({})
|
||
headings = [b['text'] for b in blocks if b.get('kind') == 'heading']
|
||
assert 'Design Criteria' in headings
|
||
assert headings[0] == 'Design Criteria'
|
||
|
||
|
||
def test_prime_notation_used_not_ascii_apostrophe():
|
||
blocks, _problems = pc.page({})
|
||
for b in _walk(blocks):
|
||
if b.get('kind') != 'field':
|
||
continue
|
||
symbol = b.get('symbol') or ''
|
||
# f'c / f'ci style ASCII apostrophes must never appear; the real
|
||
# prime character (U+2032) is what the renderer turns into a
|
||
# subscript.
|
||
assert "f'c" not in symbol and "f'ci" not in symbol
|
||
if u"f′" in symbol or u"E′" in symbol:
|
||
assert True # sanity: the prime character is reachable at all
|
||
|
||
|
||
def test_load_combination_tables_have_include_checkboxes():
|
||
blocks, problems = pc.page({})
|
||
tables = [b for b in _walk(blocks) if b.get('kind') == 'lc_table']
|
||
assert len(tables) == 2
|
||
assert set(t['name'] for t in tables) == set(['strength', 'service'])
|
||
for t in tables:
|
||
assert t['headers'][-1] == 'Include'
|
||
for row in t['rows']:
|
||
cell = row[-1]
|
||
assert isinstance(cell, dict) and cell['editor'] == 'check'
|
||
assert cell['key'].startswith(
|
||
'load_combo.{0}.'.format(t['name']))
|
||
assert cell['key'].endswith('.include')
|
||
# The Include ticks used to be reported as having "no backing fields yet".
|
||
# They do now (`panelanalysisform.load_combo_include_key`, pre-ticked by
|
||
# `prefill`), so that problem is gone rather than standing stale.
|
||
joined = ' '.join(problems)
|
||
assert 'no backing fields yet' not in joined
|
||
|
||
|
||
def test_load_combination_factor_cells_are_editable_and_keyed():
|
||
"""Owner, 2026-09-03: the ACI factors are the user's to change. Every
|
||
factor cell must be a live numeric editor with a key of its own, not the
|
||
plain printed text the old `table` block carried."""
|
||
blocks, _problems = pc.page({})
|
||
tables = [b for b in _walk(blocks) if b.get('kind') == 'lc_table']
|
||
assert tables
|
||
for t in tables:
|
||
assert t['factors'] == ['D', 'L', 'R', 'W', 'E', 'T', 'H']
|
||
for row in t['rows']:
|
||
rid = row[0]
|
||
assert isinstance(rid, str) and rid
|
||
for i, col in enumerate(t['factors']):
|
||
cell = row[i + 1]
|
||
assert isinstance(cell, dict), (
|
||
'factor cell {0} of row {1} is not an editor'
|
||
.format(col, rid))
|
||
assert cell['editor'] == 'num'
|
||
assert cell['key'] == 'load_combo.{0}.{1}.{2}'.format(
|
||
t['name'], rid, col)
|
||
|
||
|
||
def test_load_combination_defaults_match_the_form_modules_table():
|
||
"""The local ACI tuples are a fallback for an unimportable sibling module
|
||
(see `_combo_table`); they must not drift from the live copy."""
|
||
from vibe import panelanalysisform as paf
|
||
assert pc._STRENGTH_COMBOS == paf.LOAD_COMBO_ROWS['strength']
|
||
assert pc._SERVICE_COMBOS == paf.LOAD_COMBO_ROWS['service']
|
||
|
||
|
||
def test_a_user_added_row_shows_up_in_the_table():
|
||
"""A row the user added exists only as keys in `values`; the table has to
|
||
rebuild it from there or the row would vanish on the next render."""
|
||
values = {'load_combo.strength.u1.include': 'yes',
|
||
'load_combo.strength.u1.D': '1.05'}
|
||
blocks, _problems = pc.page(values)
|
||
strength = [b for b in _walk(blocks)
|
||
if b.get('kind') == 'lc_table' and b['name'] == 'strength'][0]
|
||
ids = [row[0] for row in strength['rows']]
|
||
assert ids[-1] == 'u1'
|
||
assert len(ids) == 9 # the eight ACI rows, then the user's
|
||
|
||
|
||
def test_soil_blocks_are_sentences_not_editors():
|
||
blocks, problems = pc.page({})
|
||
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
|
||
assert any('no exterior soil loads' in s for s in sentences)
|
||
assert any('no interior soil loads' in s for s in sentences)
|
||
assert any('soil loads are not collected' in p for p in problems)
|
||
|
||
|
||
def test_prestress_loss_group_is_a_sentence_when_blank():
|
||
blocks, _problems = pc.page({})
|
||
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
|
||
assert any('No prestress loss assumptions' in s for s in sentences)
|
||
# And its fields must NOT be rendered while blank -- shown only once
|
||
# something in the group is filled in.
|
||
keys = set(_field_keys(blocks))
|
||
assert 'ps_loss_method' not in keys
|
||
|
||
|
||
def test_prestress_loss_group_becomes_fields_when_filled():
|
||
blocks, _problems = pc.page({'ps_loss_method': 'PCI'})
|
||
keys = set(_field_keys(blocks))
|
||
assert 'ps_loss_method' in keys
|
||
assert 'ps_loss_humidity_pct' in keys
|
||
|
||
|
||
def test_filling_one_group_field_does_not_file_a_phantom_problem_for_others():
|
||
# Regression guard: `_group_or_sentence` must not evaluate the
|
||
# filled-in branch (and thus call `F(key)`, which files a "missing"
|
||
# problem) when the sentence branch is the one actually shown.
|
||
_blocks, problems_blank = pc.page({})
|
||
joined = ' '.join(problems_blank)
|
||
assert 'ps_loss_method' not in joined
|
||
assert 'ps_loss_humidity_pct' not in joined
|
||
|
||
|
||
def test_rvalue_and_min_reinforcement_default_sentences_present():
|
||
blocks, _problems = pc.page({})
|
||
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
|
||
assert any('R-value is not being calculated' in s for s in sentences)
|
||
assert any('Minimum steel is not being checked' in s for s in sentences)
|
||
|
||
|
||
def test_concrete_grid_has_release_and_inplace_strength_fields():
|
||
blocks, _problems = pc.page({})
|
||
keys = set(_field_keys(blocks))
|
||
assert 'fci_psi' in keys
|
||
assert 'fc_psi' in keys
|
||
assert 'conc_eci_psi' in keys
|
||
assert 'conc_ec_psi' in keys
|
||
|
||
|
||
def test_ksi_reconciliation_hint_on_psi_keys():
|
||
blocks, _problems = pc.page({})
|
||
for b in _walk(blocks):
|
||
if b.get('kind') == 'field' and b['key'] in ('fc_psi', 'fci_psi'):
|
||
assert 'ksi' in b['hint'].lower()
|
||
|
||
|
||
def test_required_fields_match_panelanalysisform_where_it_exists():
|
||
from vibe import panelanalysisform as pf
|
||
blocks, _problems = pc.page({})
|
||
required_here = dict((b['key'], b['required'])
|
||
for b in _walk(blocks) if b.get('kind') == 'field')
|
||
for f in pf.FIELDS:
|
||
key = f['key']
|
||
if key in required_here:
|
||
assert required_here[key] == f['required'], (
|
||
'required flag mismatch for {0!r}'.format(key))
|
||
|
||
|
||
def test_panel_header_kv_shown_only_when_reads_present():
|
||
blocks_no_reads, _ = pc.page({}, {})
|
||
kvs_no_reads = [b for b in blocks_no_reads if b.get('kind') == 'kv']
|
||
blocks_with_reads, _ = pc.page({}, {'mark': 'sample_mark'})
|
||
kvs_with_reads = [b for b in blocks_with_reads if b.get('kind') == 'kv']
|
||
assert len(kvs_with_reads) >= len(kvs_no_reads) + 1
|
||
|
||
|
||
def test_page_import_survives_a_broken_panelanalysisform(monkeypatch):
|
||
"""Simulates the sibling agent's file being mid-edit / unimportable.
|
||
|
||
`_existing_keys()` is the one function that reaches for
|
||
`panelanalysisform`; monkeypatching it directly to behave the way it
|
||
would if the import raised (returning `None`) exercises the same
|
||
"could not judge" path `page()` must degrade through, without depending
|
||
on the interpreter's module-cache mechanics for a package (`vibe`)
|
||
that other tests in this same process have already imported
|
||
`panelanalysisform` onto as an attribute.
|
||
"""
|
||
monkeypatch.setattr(pc, '_existing_keys', lambda: None)
|
||
blocks, problems = pc.page({})
|
||
assert isinstance(blocks, list) and blocks
|
||
assert any('could not read panelanalysisform' in p for p in problems)
|
||
|
||
|
||
def test_choice_field_has_choices():
|
||
blocks, _problems = pc.page({})
|
||
for b in _walk(blocks):
|
||
if b.get('kind') == 'field' and b['key'] == 'concrete_type':
|
||
assert b['editor'] == 'choice'
|
||
assert 'Normalweight' in b['choices']
|
||
|
||
|
||
# ------------------------------------------------ 2026-09-04 dropdown pass
|
||
#
|
||
# Owner: "很多东西应该是选项,而不是让用户填写" -- governing code, ASCE
|
||
# chapters and the rest of the enumerated Design Criteria settings become
|
||
# pick-lists. Vocabulary is pinned exactly because `vibe.pa_defaults` (a
|
||
# sibling module) writes these values verbatim.
|
||
|
||
def test_design_spec_is_a_choice_field():
|
||
blocks, _problems = pc.page({})
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
f = fields['design_spec']
|
||
assert f['editor'] == 'choice'
|
||
assert f['choices'] == ['ACI 318-19', 'ACI 318-14', 'ACI 318-11']
|
||
|
||
|
||
def test_design_criteria_settings_fields_are_choice_editors():
|
||
blocks, _problems = pc.page({})
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
expected = {
|
||
'design_criteria.analysis_beta_d': ['Non-Sway', 'Sway'],
|
||
'design_criteria.cracking_method': ['Mast Method', 'Uncracked',
|
||
'Cracked'],
|
||
'design_criteria.flexure_method': ['Whitney Stress Block',
|
||
'Strain Compatibility'],
|
||
'design_criteria.flexure_cracking_check': ['None', '1.2 Mcr',
|
||
'1.33 Mu'],
|
||
'design_criteria.deflection_cantilever_limit': ['L/75', 'L/120',
|
||
'L/150'],
|
||
}
|
||
for key, choices in expected.items():
|
||
f = fields[key]
|
||
assert f['editor'] == 'choice', '{0} is not a choice editor'.format(
|
||
key)
|
||
assert f['choices'] == choices
|
||
|
||
|
||
def test_yes_no_design_criteria_fields_stay_checkboxes():
|
||
blocks, _problems = pc.page({})
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
for key in (
|
||
'design_criteria.deflection_add_bow',
|
||
'design_criteria.deflection_check_cantilever',
|
||
'design_criteria.rvalue_calc_enabled',
|
||
'design_criteria.min_reinf_checked',
|
||
'design_criteria.stress_failure_if_cracked',
|
||
'design_criteria.stress_ignore_lateral_ecc',
|
||
'design_criteria.stress_use_principal_axis',
|
||
'design_criteria.shear_treat_as_column',
|
||
'design_criteria.axial_ignore_tensile_failures',
|
||
'design_criteria.axial_ignore_buckling_failures',
|
||
):
|
||
assert fields[key]['editor'] == 'check', key
|
||
assert fields[key]['choices'] is None
|
||
|
||
|
||
def test_cracking_assumption_now_renders_as_a_choice_field():
|
||
"""`cracking_assumption` is a real `panelanalysisform.FIELDS` key with a
|
||
Cracked/Uncracked vocabulary (and a `PREFILL_DEFAULTS` entry) that no
|
||
page module surfaced before this change -- added beside `second_order`
|
||
in Structural Analysis so a pre-filled value has somewhere to be seen
|
||
and edited."""
|
||
blocks, _problems = pc.page({})
|
||
keys = set(_field_keys(blocks))
|
||
assert 'cracking_assumption' in keys
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
f = fields['cracking_assumption']
|
||
assert f['editor'] == 'choice'
|
||
assert f['choices'] == ['Cracked', 'Uncracked']
|
||
|
||
|
||
# ------------------------------------------------ 2026-09-04 headless review
|
||
#
|
||
# WPA655's rendered Design Criteria tab was reviewed by eye and five findings
|
||
# were filed; these tests pin the fix for each. See the module docstring's
|
||
# "UPDATE 2026-09-04" note for the Resistance Factors layout change and the
|
||
# `ps_loss_humidity_pct` comment above the Prestress Losses block.
|
||
|
||
def _table_blocks(blocks):
|
||
"""Every `table`-kind block anywhere in the tree, `columns` descended."""
|
||
return [b for b in _walk(blocks) if b.get('kind') == 'table']
|
||
|
||
|
||
def _table_cell_keys(blocks):
|
||
"""Every editor key embedded in a `table` cell. These live inside
|
||
`table.rows`, not as `field` blocks -- `_field_keys` cannot see them,
|
||
which is exactly why the Resistance Factors phi editors need their own
|
||
check rather than reusing that helper."""
|
||
keys = []
|
||
for t in _table_blocks(blocks):
|
||
for row in t.get('rows') or []:
|
||
for cell in row:
|
||
if isinstance(cell, dict) and cell.get('key'):
|
||
keys.append(cell['key'])
|
||
return keys
|
||
|
||
|
||
_PHI_KEYS = ('phi_flexure', 'design_criteria.phi_tension_transfer',
|
||
'design_criteria.phi_compression', 'phi_shear',
|
||
'design_criteria.phi_shear_transfer')
|
||
|
||
|
||
def test_every_resistance_factor_phi_has_a_visible_table_editor():
|
||
"""Finding 1: the old two-column `Developed | Transferring` `columns` of
|
||
`field` blocks squeezed every phi editor to zero width. All five phi
|
||
keys must now show up as live editors somewhere on the page."""
|
||
blocks, _problems = pc.page({})
|
||
keys = set(_table_cell_keys(blocks))
|
||
for key in _PHI_KEYS:
|
||
assert key in keys, '{0} has no visible editor'.format(key)
|
||
|
||
|
||
def test_resistance_factor_phi_cells_are_live_num_editors_not_text():
|
||
blocks, _problems = pc.page({})
|
||
cells_by_key = {}
|
||
for t in _table_blocks(blocks):
|
||
for row in t.get('rows') or []:
|
||
for cell in row:
|
||
if isinstance(cell, dict) and cell.get('key') in _PHI_KEYS:
|
||
cells_by_key[cell['key']] = cell
|
||
assert set(cells_by_key) == set(_PHI_KEYS)
|
||
for key, cell in cells_by_key.items():
|
||
assert cell['editor'] == 'num', '{0} is not a num editor'.format(key)
|
||
|
||
|
||
def test_resistance_factors_table_marks_compression_transferring_na():
|
||
"""The one phi that genuinely does not apply (compression once the
|
||
strand has fully transferred) stays a plain text cell, not an editor --
|
||
same discipline as the old `sentence`, just inside the new table."""
|
||
blocks, _problems = pc.page({})
|
||
tables = [t for t in _table_blocks(blocks) if t['headers'][0] == u'φ']
|
||
assert len(tables) == 1
|
||
rows = dict((row[0], row) for row in tables[0]['rows'])
|
||
assert 'Compression' in rows
|
||
na_cell = rows['Compression'][2] # the 'Transferring' column
|
||
assert isinstance(na_cell, str)
|
||
assert 'not applicable' in na_cell.lower()
|
||
|
||
|
||
def test_resistance_factor_missing_backing_field_problems_still_filed():
|
||
"""Moving the three `design_criteria.phi_*` keys into table cells must
|
||
not silently drop the existing 'no backing field yet' bookkeeping --
|
||
`_cell()` routes every key through `F()` first specifically to keep it."""
|
||
_blocks, problems = pc.page({})
|
||
joined = ' '.join(problems)
|
||
for key in ('design_criteria.phi_tension_transfer',
|
||
'design_criteria.phi_compression',
|
||
'design_criteria.phi_shear_transfer'):
|
||
assert key in joined, '{0} missing-field problem was dropped'.format(
|
||
key)
|
||
|
||
|
||
def test_mesh_material_fields_are_collected():
|
||
"""Finding 2: `mesh_fy_ksi` / `mesh_es_ksi` exist in
|
||
`panelanalysisform.FIELDS` (80 ksi / 29000 ksi defaults) but this page
|
||
used to render only a 'not collected yet' sentence for Mesh -- now shown
|
||
the same way Rebar's fy / Es are."""
|
||
blocks, problems = pc.page({})
|
||
keys = set(_field_keys(blocks))
|
||
assert 'mesh_fy_ksi' in keys
|
||
assert 'mesh_es_ksi' in keys
|
||
joined = ' '.join(problems)
|
||
# `_field()`'s missing-backing-field message quotes the key inside
|
||
# parens -- "(mesh_fy_ksi)" -- and must not appear, since both keys are
|
||
# real `panelanalysisform.FIELDS` entries.
|
||
assert '(mesh_fy_ksi)' not in joined
|
||
assert '(mesh_es_ksi)' not in joined
|
||
sentences = [b['text'] for b in _walk(blocks) if b.get('kind') == 'sentence']
|
||
assert not any('No mesh material properties' in s for s in sentences)
|
||
|
||
|
||
def test_inplace_tension_sqrt_row_present_above_modulus_of_rupture():
|
||
"""Finding 3: the uncracked in-place tension limit
|
||
(`inplace_tension_sqrt`, 7.5 sqrt(f'c)) is a real `panelanalysisform`
|
||
key this page never surfaced. All three In-Place Stress Limits rows
|
||
must be present and named the way Eriksson prints them."""
|
||
blocks, _problems = pc.page({})
|
||
keys = set(_field_keys(blocks))
|
||
assert 'inplace_tension_sqrt' in keys
|
||
assert 'design_criteria.inplace_modulus_rupture_sqrt_fc' in keys
|
||
assert 'inplace_comp_ratio' in keys
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
assert fields['inplace_tension_sqrt']['label'] == u'Tension, uncracked'
|
||
assert (fields['design_criteria.inplace_modulus_rupture_sqrt_fc']['label']
|
||
== u'Tension, transition (modulus of rupture)')
|
||
assert fields['inplace_comp_ratio']['label'] == u'Compression'
|
||
|
||
|
||
def test_inplace_tension_sqrt_precedes_modulus_of_rupture_in_col_b():
|
||
"""Order matters, not just presence -- the uncracked tension row belongs
|
||
above the modulus-of-rupture row, as on Eriksson's own printout."""
|
||
blocks, _problems = pc.page({})
|
||
field_keys = _field_keys(blocks)
|
||
i_tension = field_keys.index('inplace_tension_sqrt')
|
||
i_rupture = field_keys.index(
|
||
'design_criteria.inplace_modulus_rupture_sqrt_fc')
|
||
assert i_tension < i_rupture
|
||
|
||
|
||
def test_prestress_loss_percent_symbol_field_still_present_and_editable():
|
||
"""Finding 4: the '%' unit on `ps_loss_humidity_pct` must stay attached
|
||
to a live editor after the group's fields moved from one wide column
|
||
into a 2x2 layout -- only the layout changed, not the field itself."""
|
||
blocks, _problems = pc.page({'ps_loss_method': 'PCI'})
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
f = fields['ps_loss_humidity_pct']
|
||
assert f['symbol'] == '%'
|
||
assert f['editor'] == 'num'
|
||
|
||
|
||
def test_sway_condition_field_is_a_choice_editor():
|
||
"""Finding 6: keep the label, make sure the choice editor is used."""
|
||
blocks, _problems = pc.page({})
|
||
fields = dict((b['key'], b) for b in _walk(blocks)
|
||
if b.get('kind') == 'field')
|
||
f = fields['design_criteria.analysis_beta_d']
|
||
assert f['label'] == u'Sway condition for 2nd-order analysis'
|
||
assert f['symbol'] == u'βd'
|
||
assert f['editor'] == 'choice'
|
||
assert f['choices'] == ['Non-Sway', 'Sway']
|