Files
willtools-demo/WillTools.extension/tests/test_panelanalysisform.py
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
2026-09-03 16:33:47 -05:00

701 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Host tests for `vibe.panelanalysisform` -- the Panel Analysis input form's
pure logic: pre-fill, the completeness gate, the deck build, the Analyze-All
batch plan, the plain-language section/group structure, the Handling stage
matrix, and the openings / strand-group row parsers.
The WPF form itself (the accordion, the per-panel Run Analysis buttons, the
live geometry read off the framing element) is owner-must-verify in Revit;
everything below the form boundary is here. Loaded by path like the sibling
`test_panelanalysis*` files -- host CPython reaching `tools/` for the schema so
a deck this module builds can be fed straight back through the real
`panelanalysis/1` reader and solve.
"""
from __future__ import annotations
import os
import sys
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TOOLS = os.path.join(ROOT, 'tools')
PANELLIFT_SRC = os.path.join(ROOT, 'panellift', 'src')
for p in (TOOLS, PANELLIFT_SRC):
if p not in sys.path:
sys.path.insert(0, p)
from vibe import knowledge # noqa: E402,F401 (registers the vibe stub)
from vibe import panelanalysisform as F # noqa: E402
schema = pytest.importorskip('panelanalysis_schema')
statics = pytest.importorskip('panellift.statics')
# A model-read for a plain solid panel: geometry + materials the framing
# element supplied, supports blank (the user types those). Generic values.
READS = {
'mark': 'PNL101',
'design_number': 'PNL1',
'length_ft': 24.0,
'width_ft': 8.0,
'thickness_in': 8.0,
'fc_psi': 6000.0,
'fci_psi': 3500.0,
'unit_weight_pcf': 150.0,
}
def _complete_values():
"""A fully-fillable form: prefilled reads + the user's supports."""
values = F.prefill(READS)
values['supports_ft'] = '2, 22'
return values
def _with_prestress(values=None, count='4', dia='1/2', pull='28900',
rows='4 @ 2'):
values = dict(values or _complete_values())
values['ps_count'] = count
values['ps_dia_in'] = dia
values['ps_pull_lb'] = pull
values['ps_rows'] = rows
return values
# --------------------------------------------------------------- structure
def test_sections_are_the_five_eriksson_names_and_cover_every_field():
names = [name for name, _groups in F.sections()]
assert names == list(F.SECTION_ORDER)
assert names == ['Design Criteria', 'Concrete Extents',
'Structural Model', 'Handling', 'Reinforcement']
covered = set()
for _name, groups in F.sections():
for _group_name, fields in groups:
for f in fields:
covered.add(f['key'])
field_keys = set(f['key'] for f in F.FIELDS)
assert covered == field_keys
def test_sections_group_fields_and_every_group_is_named_or_none():
for _name, groups in F.sections():
group_names = [g for g, _fields in groups]
# at most one ungrouped (None) bucket, and it comes first if present
assert group_names.count(None) <= 1
if None in group_names:
assert group_names[0] is None
# every named group is non-empty
for g, fields in groups:
assert fields
if g is not None:
assert isinstance(g, str) and g
def test_design_criteria_has_the_four_named_groups():
by_section = dict(F.sections())
dc_groups = [g for g, _f in by_section['Design Criteria']]
for want in ('Material Properties', 'Prestress Losses',
'Load Combinations', 'Stress Limits & Factors'):
assert want in dc_groups
def test_every_field_declares_a_known_section_group_and_drives_flag():
for f in F.FIELDS:
assert f['section'] in F.SECTION_ORDER, f['key']
assert f['drives'] in ('solve', 'stage3', 'info'), f['key']
assert f['kind'] in ('text', 'num', 'int', 'supports', 'openings',
'strands'), f['key']
assert isinstance(f['label'], str) and f['label']
assert isinstance(f['help'], str) and f['help']
# the plain label carries no raw prime/subscript math -- that lives
# in `symbol` instead (the owner's "太丑了" ruling)
assert u'′' not in f['label'], f['key']
def test_wind_fields_are_present_blank_and_collected_not_yet_driving():
wind = [f for f in F.FIELDS if f['key'].startswith('wind_')]
keys = set(f['key'] for f in wind)
for want in ('wind_speed_vult', 'wind_risk_category', 'wind_exposure',
'wind_enclosure', 'wind_asce_version', 'wind_cc_chapter',
'wind_mwfrs_chapter', 'wind_pressure_psf', 'wind_suction_psf'):
assert want in keys
for f in wind:
assert f['section'] == 'Structural Model'
assert f['group'] == 'Wind'
assert f['drives'] == 'stage3' # collected, not yet driving
values = F.prefill(READS)
for k in keys:
assert values[k] == ''
def test_strip_transport_erection_sling_fields_are_gone():
# migrated INTO the stage matrix, not left sitting beside it
keys = set(f['key'] for f in F.FIELDS)
for gone in ('strip_mult', 'transport_mult', 'erection_mult',
'sling_angle', 'ps_ecc_in'):
assert gone not in keys
# ----------------------------------------------------------------- prefill
def test_prefill_fills_readable_and_blanks_the_rest():
values = F.prefill(READS)
assert values['length_ft'] == '24'
assert values['width_ft'] == '8'
assert values['thickness_in'] == '8'
assert values['fc_psi'] == '6000'
assert values['fci_psi'] == '3500'
assert values['mark'] == 'PNL101'
# supports and every stage3 / load field come up blank for the user
assert values['supports_ft'] == ''
assert values['self_weight_factor'] == ''
assert values['ps_count'] == ''
assert values['ps_rows'] == ''
assert values['openings'] == ''
def test_prefill_blanks_a_field_the_model_could_not_read():
reads = dict(READS)
reads['fci_psi'] = None
del reads['unit_weight_pcf']
values = F.prefill(reads)
assert values['fci_psi'] == ''
assert values['unit_weight_pcf'] == ''
def test_prefill_blanks_every_stage_matrix_cell():
values = F.prefill(READS)
for key in F.stage_field_keys():
assert values[key] == ''
# -------------------------------------------------------------- validation
def test_complete_form_validates_and_parses():
errors, parsed = F.validate(_complete_values())
assert errors == []
assert parsed['supports'] == [2.0, 22.0]
assert parsed['length_ft'] == 24.0
assert parsed['fc_psi'] == 6000.0
assert parsed['openings_rows'] == []
assert parsed['strand_rows'] == []
def test_blank_required_field_blocks_and_is_named():
values = _complete_values()
values['fc_psi'] = ''
errors, parsed = F.validate(values)
assert parsed is None
# The label now reads plain English; the symbol (f′c) still rides beside
# it via `display_label`. What is pinned here is that the message names
# the field, in BOTH vocabularies.
assert any('Concrete strength at 28 days' in e and 'required' in e
for e in errors)
assert any(u'f′c' in e for e in errors)
def test_fewer_than_two_supports_is_refused_by_name():
values = _complete_values()
values['supports_ft'] = '2'
errors, parsed = F.validate(values)
assert parsed is None
assert any('at least two support' in e for e in errors)
assert not F.is_complete(values)
def test_no_supports_is_refused():
values = _complete_values()
values['supports_ft'] = ''
errors, parsed = F.validate(values)
assert parsed is None
assert any('support' in e.lower() for e in errors)
def test_garbage_support_token_is_named():
values = _complete_values()
values['supports_ft'] = '2, banana'
errors, parsed = F.validate(values)
assert parsed is None
assert any('banana' in e for e in errors)
def test_stage3_field_never_blocks_the_run():
values = _complete_values()
# a wind value typed in but the solve does not use it -> still runnable,
# and even a non-numeric stray in a stage3 field must not block
values['wind_speed_vult'] = 'not a number'
values['wind_risk_category'] = 'II'
assert F.is_complete(values)
def test_bad_number_in_a_solve_field_blocks():
values = _complete_values()
values['unit_weight_pcf'] = 'heavy'
errors, parsed = F.validate(values)
assert parsed is None
assert any('Concrete unit weight' in e for e in errors)
# -------------------------------------------------------------- prestress
def test_no_prestress_fields_means_no_prestress_block():
_errors, parsed = F.validate(_complete_values())
assert parsed['prestress'] is None
assert parsed['strand_rows'] == []
def test_full_prestress_group_builds_strands():
values = _with_prestress(rows='4 @ 2')
errors, parsed = F.validate(values)
assert errors == []
ps = parsed['prestress']
assert len(ps['strands']) == 4
assert all(s['area_in2'] == 0.153 for s in ps['strands'])
assert all(s['initial_pull_lb'] == 28900.0 for s in ps['strands'])
assert all(s['z_in'] == 2.0 for s in ps['strands'])
def test_prestress_group_with_multiple_rows_and_per_row_pull_pct():
values = _with_prestress(count='8', rows='4 @ 2 @ 75\n4 @ 9.25 @ 56')
values['ps_fpu_ksi'] = '270'
errors, parsed = F.validate(values)
assert errors == []
ps = parsed['prestress']
assert len(ps['strands']) == 8
shallow = [s for s in ps['strands'] if s['z_in'] == 2.0]
deep = [s for s in ps['strands'] if s['z_in'] == 9.25]
assert len(shallow) == 4 and len(deep) == 4
# pull = pct/100 * fpu(ksi)*1000 * area, not the default ps_pull_lb
assert all(abs(s['initial_pull_lb'] - 0.75 * 270.0 * 1000.0 * 0.153) < 1
for s in shallow)
assert all(abs(s['initial_pull_lb'] - 0.56 * 270.0 * 1000.0 * 0.153) < 1
for s in deep)
def test_partial_prestress_group_is_refused_field_by_field():
values = _complete_values()
values['ps_count'] = '4' # started, but no size/pull/rows
errors, parsed = F.validate(values)
assert parsed is None
assert any('diameter' in e for e in errors)
assert any('pull' in e for e in errors)
assert any('Strand groups' in e for e in errors)
def test_unknown_strand_size_is_refused_not_guessed():
values = _with_prestress(count='2', dia='0.99', rows='2 @ 2')
errors, parsed = F.validate(values)
assert parsed is None
assert any('section area' in e for e in errors)
def test_ps_count_disagreement_with_rows_is_reported_not_resolved():
values = _with_prestress(count='5', rows='4 @ 2') # 5 != 4
errors, parsed = F.validate(values)
assert parsed is None
assert any('does not match the sum' in e for e in errors)
def test_ps_count_blank_skips_the_cross_check():
values = _with_prestress(count='', rows='4 @ 2')
errors, parsed = F.validate(values)
assert errors == []
assert len(parsed['prestress']['strands']) == 4
def test_garbage_strand_row_line_is_named_by_line_number():
values = _with_prestress(rows='4 @ 2\nbanana')
errors, parsed = F.validate(values)
assert parsed is None
assert any('line 2' in e for e in errors)
def test_strand_rows_are_kept_in_parsed_even_when_group_incomplete():
values = _complete_values()
values['ps_rows'] = '4 @ 2' # rows typed, but no dia/pull -> refused
errors, parsed = F.validate(values)
assert parsed is None # the whole panel is refused (as before)
# but strand_rows / ps_rows are not something to re-derive here; the
# raw text is preserved off `values` regardless, and validate() does not
# crash trying to reach into it.
# ------------------------------------------------------------- openings
def test_blank_openings_is_no_openings():
values = _complete_values()
values['openings'] = ''
errors, parsed = F.validate(values)
assert errors == []
assert parsed['openings_rows'] == []
assert parsed['openings'] == ''
def test_one_opening_per_line_parses_the_new_grammar():
rows, problems = F.parse_openings(
'Opening, 2, start, 3, 4, left, 5, 1, 6, Door\n'
'Reveal, 1, end, 2, 1, right, 2, 0, 1')
assert problems == []
assert len(rows) == 2
door = rows[0]
assert door == {'type': 'Opening', 'pos_along': 2.0,
'pos_along_from': 'start', 'length': 3.0,
'pos_cross': 4.0, 'pos_cross_from': 'left',
'width': 5.0, 'pos_vert': 1.0, 'height': 6.0,
'label': 'Door'}
reveal = rows[1]
assert reveal['type'] == 'Reveal'
assert reveal['pos_along_from'] == 'end'
assert reveal['pos_cross_from'] == 'right'
assert reveal['label'] == ''
def test_openings_bad_type_is_named_by_line_number():
rows, problems = F.parse_openings('Doorhole, 2, start, 3, 4, left, 5, 1, 6')
assert rows == []
assert any('line 1' in p and 'type' in p for p in problems)
def test_openings_bad_datum_word_is_named():
rows, problems = F.parse_openings(
'Opening, 2, sideways, 3, 4, left, 5, 1, 6')
assert rows == []
assert any('pos_along_from' in p for p in problems)
def test_openings_all_or_nothing_across_lines():
rows, problems = F.parse_openings(
'Opening, 2, start, 3, 4, left, 5, 1, 6\nbanana')
assert rows == [] # the good line is not kept either
assert len(problems) == 1
assert 'line 2' in problems[0]
def test_openings_row_reaches_validate_parsed_output():
values = _complete_values()
values['openings'] = 'Opening, 2, start, 3, 4, left, 5, 1, 6, Door'
errors, parsed = F.validate(values)
assert errors == []
assert parsed['openings_rows'] == [{
'type': 'Opening', 'pos_along': 2.0, 'pos_along_from': 'start',
'length': 3.0, 'pos_cross': 4.0, 'pos_cross_from': 'left',
'width': 5.0, 'pos_vert': 1.0, 'height': 6.0, 'label': 'Door'}]
assert parsed['openings'] == values['openings']
def test_bad_openings_line_blocks_the_run_and_names_the_line():
values = _complete_values()
values['openings'] = 'banana'
errors, parsed = F.validate(values)
assert parsed is None
assert any('line 1' in e for e in errors)
# ------------------------------------------------------------ stage matrix
def test_stage_matrix_shape():
m = F.stage_matrix()
assert m['stages'] == ['Stripping', 'Transportation', 'Primary Erection',
'Secondary Erection']
opt_keys = [r['opt_key'] for r in m['rows']]
assert opt_keys == ['include', 'load_multiplier', 'lift_type',
'concrete_strength', 'stress_limits', 'rigging',
'sling_angle', 'ground_reaction', 'min_tilt_deg',
'max_tilt_deg', 'top_crane_carries']
for row in m['rows']:
assert set(row['cells'].keys()) == set(m['stages'])
for stage, key in row['cells'].items():
assert key == F.stage_field_key(stage, row['opt_key'])
def test_stage_matrix_offers_choices_but_never_a_default():
m = F.stage_matrix()
by_key = dict((r['opt_key'], r) for r in m['rows'])
assert by_key['lift_type']['choices'] == ('Static Lift', 'Rolling Block')
assert by_key['include']['choices'] is None
values = F.prefill(READS)
for key in F.stage_field_keys():
assert values[key] == '' # nothing pre-selected
def test_stage_matrix_load_multiplier_rides_into_deck():
values = _complete_values()
values[F.stage_field_key('Stripping', 'load_multiplier')] = '1.4'
values[F.stage_field_key('Transportation', 'load_multiplier')] = '1.5'
values[F.stage_field_key('Primary Erection', 'load_multiplier')] = '1.2'
errors, parsed = F.validate(values)
assert errors == []
deck = F.build_deck(parsed)
assert deck['handling']['stripping_multiplier'] == 1.4
assert deck['handling']['transport_multiplier'] == 1.5
assert deck['handling']['erection_multiplier'] == 1.2
def test_excluded_stage_does_not_gate_the_run_on_its_other_fields():
values = _complete_values()
values[F.stage_field_key('Secondary Erection', 'include')] = 'no'
values[F.stage_field_key('Secondary Erection', 'load_multiplier')] = \
'not a number'
assert F.is_complete(values)
def test_excluded_stage_keeps_the_typed_value_rather_than_dropping_it():
values = _complete_values()
values[F.stage_field_key('Secondary Erection', 'include')] = 'no'
values[F.stage_field_key('Secondary Erection', 'load_multiplier')] = '9.9'
errors, parsed = F.validate(values)
assert errors == []
row = parsed['stage_matrix']['Secondary Erection']
assert row['include'] is False
assert row['load_multiplier'] == 9.9 # kept, just not required/used
def test_excluded_stage_multiplier_does_not_reach_the_deck():
values = _complete_values()
values[F.stage_field_key('Stripping', 'include')] = 'no'
values[F.stage_field_key('Stripping', 'load_multiplier')] = '1.4'
errors, parsed = F.validate(values)
assert errors == []
deck = F.build_deck(parsed)
assert 'stripping_multiplier' not in deck['handling']
def test_included_stage_bad_load_multiplier_blocks_and_names_the_stage():
values = _complete_values()
values[F.stage_field_key('Stripping', 'load_multiplier')] = 'heavy'
errors, parsed = F.validate(values)
assert parsed is None
assert any('Stripping' in e and 'Load multiplier' in e for e in errors)
def test_bad_include_answer_is_named():
values = _complete_values()
values[F.stage_field_key('Stripping', 'include')] = 'maybe'
errors, parsed = F.validate(values)
assert parsed is None
assert any('Stripping' in e for e in errors)
# ------------------------------------------------------------- deck build
def _deck_from(values):
errors, parsed = F.validate(values)
assert errors == [], errors
return F.build_deck(parsed)
def test_build_deck_is_the_panelanalysis_schema():
deck = _deck_from(_complete_values())
assert deck['schema'] == F.SCHEMA == schema.SCHEMA
schema.check_schema(deck) # does not raise
panel = schema.to_panel(deck)
assert panel.mark == 'PNL101'
assert panel.length_ft == 24.0
assert panel.fc_psi == 6000.0
def test_deck_supports_reach_the_schema():
deck = _deck_from(_complete_values())
assert schema.supports(deck) == [(2.0, 'support'), (22.0, 'support')]
def test_deck_solves_and_reactions_sum_to_load():
deck = _deck_from(_complete_values())
panel = schema.to_panel(deck)
stage = schema.handling_stage(deck, panel)
analysis = statics.analyse(panel, stage, schema.handling_rigging())
total_r = sum(line.reaction_lb for line in analysis.rig.lines)
total_load = analysis.profile.total_lb
# the closure that says the solve is real: reactions sum to the load
assert abs(total_r - total_load) <= max(1.0, 1e-6 * abs(total_load))
assert total_load > 0
def test_unbuilt_blocks_are_explicit_unread_not_fabricated():
deck = _deck_from(_complete_values())
for block in (deck['in_place'], deck['loading']['wind'],
deck['structural_model']['restraints']):
assert block['value'] is None
assert block['reason']
def test_openings_never_reach_the_deck_yet():
values = _complete_values()
values['openings'] = 'Opening, 2, start, 3, 4, left, 5, 1, 6, Door'
deck = _deck_from(values)
assert deck['concrete_extents']['blockouts'] == []
def test_prestress_deck_reads_as_a_prestress_panel():
values = _with_prestress(count='2', rows='2 @ 2')
deck = _deck_from(values)
panel = schema.to_panel(deck)
assert panel.prestress is not None
assert len(panel.prestress.strands) == 2
# ------------------------------------------------------------------ batch
# ------------------------------------------------------------------- wind
def test_new_asce_fields_are_present_blank_and_stage3():
keys = set(f['key'] for f in F.FIELDS)
for want in ('wind_mean_roof_height_ft', 'wind_effective_wind_area_ft2'):
assert want in keys
spec = F._spec(want)
assert spec['section'] == 'Structural Model'
assert spec['drives'] == 'stage3'
values = F.prefill(READS)
assert values['wind_mean_roof_height_ft'] == ''
assert values['wind_effective_wind_area_ft2'] == ''
def test_wind_gcpi_matches_windload_table_exactly():
# The form's GCpi table is a mirror of windload's; a host test pins it so
# the two published-code tables can never drift.
windload = pytest.importorskip('panellift.windload')
for key, val in windload._GCPI.items():
got, cite = F.wind_gcpi(key)
assert got == val, key
assert cite
def test_wind_gcpi_display_for_each_enclosure_and_spellings():
assert F.wind_gcpi('enclosed')[0] == 0.18
assert F.wind_gcpi('partially_enclosed')[0] == 0.55
assert F.wind_gcpi('Partially Enclosed')[0] == 0.55 # spacing/case
assert F.wind_gcpi('partial-open')[0] == 0.18 # alias + dash
assert F.wind_gcpi('open')[0] == 0.00
def test_wind_gcpi_blank_and_unknown_are_could_not_judge_not_zero():
val, why = F.wind_gcpi('')
assert val is None and why # not 0.0
val, why = F.wind_gcpi('mostly closed')
assert val is None and 'not recognized' in why
def test_resolve_wind_pressure_direct_beats_computed():
d = F.resolve_wind_pressure(31.0, 28.3)
assert d['source'] == 'direct' and d['pressure_psf'] == 31.0
def test_resolve_wind_pressure_computed_when_no_direct():
d = F.resolve_wind_pressure(None, 28.3)
assert d['source'] == 'computed' and d['pressure_psf'] == 28.3
def test_resolve_wind_pressure_not_judged_passes_reason_through():
d = F.resolve_wind_pressure(None, None, 'NOT JUDGED: h > 60 ft ...')
assert d['source'] == 'not_judged'
assert d['pressure_psf'] is None # never fabricated
assert 'NOT JUDGED' in d['reason']
def test_resolve_wind_pressure_direct_wins_even_over_not_judged():
d = F.resolve_wind_pressure(31.0, None, 'NOT JUDGED: h > 60 ft ...')
assert d['source'] == 'direct' and d['pressure_psf'] == 31.0
def test_resolve_wind_pressure_blank_when_nothing_supplied():
d = F.resolve_wind_pressure(None, None, None)
assert d['source'] == 'blank' and d['pressure_psf'] is None
assert d['reason']
def _wind_values():
values = _complete_values()
values['wind_speed_vult'] = '115'
values['wind_risk_category'] = 'II'
values['wind_exposure'] = 'C'
values['wind_enclosure'] = 'enclosed'
values['wind_asce_version'] = '7-16'
values['wind_cc_chapter'] = '30'
values['wind_mean_roof_height_ft'] = '30'
values['wind_effective_wind_area_ft2'] = '100'
return values
def test_build_deck_carries_asce_wind_inputs_when_supplied():
deck = _deck_from(_wind_values())
wind = deck['loading']['wind']
assert 'value' not in wind # no longer unread
a = wind['asce']
assert a['v_ult_mph'] == 115.0
assert a['exposure'] == 'C'
assert a['enclosure'] == 'enclosed'
assert a['code_version'] == '7-16'
assert a['mean_roof_height_ft'] == 30.0
assert a['effective_wind_area_ft2'] == 100.0
def test_build_deck_carries_direct_pressure_override():
values = _wind_values()
values['wind_pressure_psf'] = '45'
values['wind_suction_psf'] = '50'
deck = _deck_from(values)
wind = deck['loading']['wind']
assert wind['direct_pressure_psf'] == 45.0
assert wind['direct_suction_psf'] == 50.0
def test_partial_asce_without_the_required_numbers_is_not_computed():
values = _complete_values()
values['wind_exposure'] = 'C' # something typed, but no Vult/h/area
deck = _deck_from(values)
wind = deck['loading']['wind']
assert 'asce' not in wind
assert 'asce_incomplete' in wind # says what is still needed
def test_no_wind_inputs_keeps_the_explicit_unread_placeholder():
deck = _deck_from(_complete_values())
assert deck['loading']['wind']['value'] is None
assert deck['loading']['wind']['reason']
def test_restraints_field_reaches_the_deck_when_filled():
values = _wind_values()
values['restraints'] = '0, 30'
deck = _deck_from(values)
r = deck['structural_model']['restraints']
assert r == [{'x_ft': 0.0, 'type': 'restraint'},
{'x_ft': 30.0, 'type': 'restraint'}]
def test_no_restraints_keeps_the_explicit_unread_placeholder():
deck = _deck_from(_complete_values())
assert deck['structural_model']['restraints']['value'] is None
def test_batch_plan_runs_complete_and_skips_incomplete_with_reasons():
good = _complete_values()
bad = _complete_values()
bad['supports_ft'] = '5' # only one support
worse = _complete_values()
worse['fc_psi'] = '' # missing material
named = [('PNL101', good), ('PNL102', bad), ('PNL103', worse)]
to_run, skipped = F.batch_plan(named)
run_labels = [lbl for lbl, _p in to_run]
skip_labels = [lbl for lbl, _r in skipped]
assert run_labels == ['PNL101']
assert set(skip_labels) == {'PNL102', 'PNL103'}
reasons = dict((lbl, errs) for lbl, errs in skipped)
assert any('support' in e for e in reasons['PNL102'])
assert any(u'f′c' in e for e in reasons['PNL103'])
# ------------------------------------------------------------ display_label
def test_display_label_carries_both_label_and_symbol():
d = F.display_label('fc_psi')
assert 'Concrete strength at 28 days' in d
assert u'f′c' in d
def test_display_label_falls_back_to_label_when_no_symbol():
d = F.display_label('concrete_type')
assert d == 'Concrete type'