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.
334 lines
16 KiB
Python
334 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""`ticketchecks.REINFORCING_CHECKS` / `REINFORCING_EXCLUDED` / `run_reinforcing`.
|
|
|
|
Same trick as `test_ticketchecks_host.py`, and the same reason: `ticketqc`
|
|
imports `Autodesk.Revit.DB` at module scope and `ticketchecks` takes
|
|
`Finding` from it, so neither imports on host CPython without an inert
|
|
stub standing in for the Revit API. See that file's own docstring for why
|
|
the stub is safe here -- nothing this file exercises touches Revit either.
|
|
|
|
WHAT THIS FILE IS FOR, beyond the mechanical assertions: the two sets below
|
|
(`EXPECTED_INCLUDE_REASONS` / `EXPECTED_EXCLUDE_REASONS`) are the
|
|
check-by-check classification this task was asked to produce, written down
|
|
so a change to either list in `ticketchecks.py` is a visible test failure
|
|
here rather than a silent drift between the code and the report the owner
|
|
was handed. Adjusting the classification is expected to mean editing BOTH
|
|
`ticketchecks.py` and this file's tables together.
|
|
|
|
RE-CUT 2026-09-07 against a real bridge run on three panels (WPA655,
|
|
WPB530E, SMD516 -- see the module note above
|
|
`ticketchecks.REINFORCING_EXCLUDED`). Two checks moved from INCLUDE to
|
|
EXCLUDE (`check_cast_extras`, `check_weight_three_way`) because the run
|
|
showed both to be, in practice, a ticket-vs-model comparison rather than a
|
|
reinforcing-stage one. This file also gained coverage for the MEMBER-CLASS
|
|
split added the same day (`ticketchecks.member_class_of` /
|
|
`REINFORCING_APPLIES_TO` / `NOT_FOR_CLASS_DISPOSITION`).
|
|
"""
|
|
# pylint: disable=import-error,invalid-name,broad-except
|
|
import sys
|
|
import types
|
|
|
|
import pytest
|
|
|
|
|
|
def _stub_revit():
|
|
if 'Autodesk.Revit.DB' in sys.modules:
|
|
return []
|
|
autodesk = types.ModuleType('Autodesk')
|
|
revit = types.ModuleType('Autodesk.Revit')
|
|
db = types.ModuleType('Autodesk.Revit.DB')
|
|
for name in ('FilteredElementCollector', 'AssemblyInstance', 'View',
|
|
'ViewSheet', 'ViewType', 'BuiltInCategory', 'Element',
|
|
'StorageType', 'ElementId', 'XYZ', 'Transform',
|
|
'Options', 'Solid', 'BoundingBoxXYZ'):
|
|
setattr(db, name, type(str(name), (object,), {}))
|
|
autodesk.Revit = revit
|
|
revit.DB = db
|
|
sys.modules['Autodesk'] = autodesk
|
|
sys.modules['Autodesk.Revit'] = revit
|
|
sys.modules['Autodesk.Revit.DB'] = db
|
|
return ['Autodesk.Revit.DB', 'Autodesk.Revit', 'Autodesk']
|
|
|
|
|
|
_ADDED = _stub_revit()
|
|
try:
|
|
from vibe import ticketchecks as TC # noqa: E402
|
|
finally:
|
|
for _name in _ADDED:
|
|
sys.modules.pop(_name, None)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# THE CLASSIFICATION TABLE. Every one of the 41 functions in `TC.CHECKS`,
|
|
# one bucket or the other, with the checklist row(s) it owns (from
|
|
# `knowledge/catalogue.yaml`'s `checklist.master`, read 2026-09-05) noted in
|
|
# a comment beside it for a human cross-checking this against the report.
|
|
|
|
# INCLUDED: evidence is the panel's modelled content -- geometry, assembly
|
|
# membership, nested sub-components, and the design intent (a spec or a
|
|
# design-summary view, which describe the DESIGN, not a drawn ticket).
|
|
EXPECTED_INCLUDE = frozenset([
|
|
'check_connectors_exist', # 5.8 modelled connectors
|
|
'check_connectors_sufficient', # 5.9 modelled connector count/spacing
|
|
'check_connector_edge_distance', # 5.10 modelled connector position
|
|
'check_solid_zone_members', # 5.11 modelled assembly membership
|
|
'check_opening_diagonal_bars', # 5.16 modelled bar-type flags
|
|
'check_wwf_designation', # 5.4 modelled mesh family
|
|
'check_bar_stock_length', # 5.7 modelled bar length parameter
|
|
'check_lifter_capacity', # 6.1 modelled lifters vs weight
|
|
'check_lifter_edge_distance', # 6.2 modelled lifters vs geometry
|
|
'check_handling_result', # 6.3 geometry + design intent
|
|
'check_lifter_anchor_spacing', # 6.7 modelled lifter spacing
|
|
'check_lifting_points', # 6.9 (MC) modelled lifter count
|
|
'check_lift_positions', # 6.10 (MC) modelled lifter position
|
|
'check_lifter_accessory_in_assembly', # 6.13 modelled nested-child
|
|
# assembly membership
|
|
'check_member_interference', # 7.1 modelled geometry clash
|
|
'check_release_ratio', # 3.3 design intent only
|
|
'check_spec_unresolved', # 8.1 design spec/summary, no ticket
|
|
'check_strand_layout', # 4.4 / 4.6 modelled strands vs intent
|
|
'check_strand_debond', # 4.7 modelled strands + geometry
|
|
])
|
|
|
|
# EXCLUDED: at least half of what the check compares is a ticket artefact --
|
|
# a sheet, a placed view's name or text, a printed call-out, a hatch pattern
|
|
# rendered on a view, a BOM schedule, a bed-plan note.
|
|
EXPECTED_EXCLUDE = frozenset([
|
|
'check_solid_zone_on_ticket', # 5.12 ticket's insulation layout view
|
|
'check_solid_zone_hatch', # 5.13 legend swatch off ticket views
|
|
'check_recess_regions', # 5.14 drafted regions on ticket views
|
|
'check_recess_hatch', # 5.15 rendered pattern off ticket views
|
|
'check_dunnage_openings', # 6.4 TextNotes on the ticket sheet
|
|
'check_dunnage_stripping_match', # 6.11 (MC) same TextNote read as 6.4
|
|
'check_bom_strand_count', # 4.8 BOM row + section print (ticket)
|
|
'check_bom_members', # 5.1 BOM schedule rows (ticket)
|
|
'check_debond_callout', # 4.5 a call-out IS ticket content
|
|
'check_picking_match', # 6.8 PICKING text-note band (ticket)
|
|
'check_detail_callouts', # 5.1 BED PLAN callout text (ticket)
|
|
'check_ticket_set', # 1.3 ticket sheet count
|
|
'check_jacking_force', # 4.3 TICKET NOTE view text
|
|
'check_bed_plan_reverse', # 5.2 bed-plan view text (ticket)
|
|
'check_sheet_name', # 1.1 the sheet's Name (ticket)
|
|
'check_invisible', # 8.2 sheet title-block strings
|
|
'check_view_name_design', # 1.4 names of views placed on ticket
|
|
'check_note_views', # 6.5 note-view content on ticket
|
|
'check_callout_overlap', # 1.5 printed call-out bounding boxes
|
|
'check_bar_mark_uniqueness', # 5.6 marks printed on the ticket
|
|
'check_parity_note', # 6.6 a phrase on the ticket BED PLAN
|
|
'check_cast_extras', # 2.4 moved 2026-09-07: a real run
|
|
# on SMD516 (has a ticket) shows
|
|
# it reading the ticket's BOM/
|
|
# view text directly, not just
|
|
# standing down without one
|
|
'check_weight_three_way', # 2.2 moved 2026-09-07: reported
|
|
# WGT-TWO-WAY (never OK) on all
|
|
# three real panels because the
|
|
# printed leg is a ticket field
|
|
# this plant never maps
|
|
])
|
|
|
|
|
|
def test_expected_tables_partition_all_checks():
|
|
"""Sanity on the tables ABOVE, before comparing them to the module: no
|
|
overlap, and every one of the 41 checks lands in exactly one bucket."""
|
|
overlap = EXPECTED_INCLUDE & EXPECTED_EXCLUDE
|
|
assert not overlap, 'named in both buckets: {0}'.format(sorted(overlap))
|
|
assert len(EXPECTED_INCLUDE) + len(EXPECTED_EXCLUDE) == len(TC.CHECKS)
|
|
|
|
|
|
def test_reinforcing_checks_is_subset_of_checks():
|
|
all_names = set(c.__name__ for c in TC.CHECKS)
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
assert reinforcing_names <= all_names
|
|
|
|
|
|
def test_reinforcing_checks_and_excluded_partition_checks():
|
|
"""Every check is included XOR excluded -- never both, never neither."""
|
|
all_names = set(c.__name__ for c in TC.CHECKS)
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
excluded_names = set(TC.REINFORCING_EXCLUDED)
|
|
assert reinforcing_names & excluded_names == set()
|
|
assert reinforcing_names | excluded_names == all_names
|
|
|
|
|
|
def test_every_excluded_check_has_a_nonempty_reason():
|
|
for name, reason in TC.REINFORCING_EXCLUDED.items():
|
|
assert isinstance(reason, str) and reason.strip(), (
|
|
'{0} has no reason, or a blank one'.format(name))
|
|
|
|
|
|
def test_classification_matches_the_report():
|
|
"""The actual module data equals the table above, name for name.
|
|
|
|
A mismatch here means `ticketchecks.py` changed without this file
|
|
being updated to match (or vice versa) -- the whole point of writing
|
|
the classification down twice.
|
|
"""
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
excluded_names = set(TC.REINFORCING_EXCLUDED)
|
|
assert reinforcing_names == EXPECTED_INCLUDE
|
|
assert excluded_names == EXPECTED_EXCLUDE
|
|
|
|
|
|
def test_run_reinforcing_returns_findings_log_and_excluded():
|
|
findings, log, excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None)
|
|
assert isinstance(findings, list)
|
|
assert isinstance(log, list)
|
|
assert excluded == dict(TC.REINFORCING_EXCLUDED)
|
|
|
|
|
|
def test_run_reinforcing_only_logs_reinforcing_checks():
|
|
"""The log never names a check `run_reinforcing` did not actually run
|
|
(or explicitly stand down) -- no leakage from the excluded set."""
|
|
_findings, log, _excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None)
|
|
logged_names = set(name for (name, _state, _note) in log)
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
assert logged_names <= reinforcing_names
|
|
|
|
|
|
def test_run_reinforcing_never_calls_check_ticket_only_row_probes():
|
|
"""`run_reinforcing` deliberately does not append `row_silences` --
|
|
those rows are all `check_ticket`'s own ticket-vs-design territory,
|
|
which this run never touches. No `row:` entry should appear."""
|
|
_findings, log, _excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None)
|
|
assert not any(name.startswith(TC.ROW_NOTE_PREFIX)
|
|
for (name, _state, _note) in log)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# MEMBER CLASS, added 2026-09-07 (owner's addendum to the same task): a
|
|
# wall panel and a flexural member answer a different checklist, and
|
|
# blurring the two is a real defect the real run showed, not a theoretical
|
|
# one -- `check_lifting_points`'s 20/25 ft point-count ceiling reported a
|
|
# FAIL against SMD516 (flexural) that a flexural member was never subject
|
|
# to.
|
|
|
|
def test_member_class_of_reads_the_mark_prefix():
|
|
assert TC.member_class_of({'mark': 'WPA655'}) == (
|
|
'wall', "mark 'WPA655' starts with 'WPA'")
|
|
assert TC.member_class_of({'mark': 'WPB530E'}) == (
|
|
'wall', "mark 'WPB530E' starts with 'WPB'")
|
|
assert TC.member_class_of({'mark': 'SMD516'}) == (
|
|
'flexural', "mark 'SMD516' starts with 'SMD'")
|
|
assert TC.member_class_of({'mark': 'FSA501'}) == (
|
|
'flexural', "mark 'FSA501' starts with 'FSA'")
|
|
cls, _basis = TC.member_class_of({'mark': 'CLA509'})
|
|
assert cls == 'column_beam'
|
|
|
|
|
|
def test_member_class_of_falls_back_to_geometry_then_gives_up():
|
|
# No recognised prefix, no plant mapping, but a long/narrow shape.
|
|
cls, basis = TC.member_class_of({'mark': 'ZZZ1', 'length': 40.0,
|
|
'width': 8.0})
|
|
assert cls == 'flexural'
|
|
assert 'geometry' in basis
|
|
# No recognised prefix, and no geometry either -- never a guess.
|
|
cls, basis = TC.member_class_of({'mark': 'ZZZ1'})
|
|
assert cls is None
|
|
assert basis
|
|
|
|
|
|
def test_member_class_of_prefers_mark_over_geometry():
|
|
# A wall-prefixed mark is trusted even if the geometry alone would have
|
|
# read as flexural -- the mark is the cheaper, more specific signal.
|
|
cls, basis = TC.member_class_of({'mark': 'WPA1', 'length': 40.0,
|
|
'width': 8.0})
|
|
assert cls == 'wall'
|
|
assert 'mark' in basis
|
|
|
|
|
|
def test_reinforcing_applies_to_restricts_only_named_checks():
|
|
# A named check is restricted...
|
|
assert not TC._reinforcing_applies_to_class(
|
|
'check_lifting_points', 'flexural')
|
|
assert TC._reinforcing_applies_to_class('check_lifting_points', 'wall')
|
|
# ...and a check named nowhere in the table applies to every class.
|
|
assert TC._reinforcing_applies_to_class('check_strand_layout', 'wall')
|
|
assert TC._reinforcing_applies_to_class('check_strand_layout', 'flexural')
|
|
|
|
|
|
def test_reinforcing_applies_to_never_narrows_an_unclassified_piece():
|
|
# member_class is None (could not judge) -- every check still runs.
|
|
assert TC._reinforcing_applies_to_class('check_lifting_points', None)
|
|
|
|
|
|
def test_reinforcing_applies_to_names_only_real_checks():
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
for name in TC.REINFORCING_APPLIES_TO:
|
|
assert name in reinforcing_names, (
|
|
'{0} is in REINFORCING_APPLIES_TO but not in '
|
|
'REINFORCING_CHECKS'.format(name))
|
|
|
|
|
|
def test_run_reinforcing_stands_down_a_wall_only_check_on_a_flexural_mark():
|
|
_findings, log, _excluded = TC.run_reinforcing(
|
|
{'mark': 'SMD516'}, {}, {}, None)
|
|
named = dict((name, (state, note)) for (name, state, note) in log)
|
|
state, note = named['check_lifting_points']
|
|
assert state == TC.NOT_FOR_CLASS_DISPOSITION
|
|
assert 'flexural' in note
|
|
|
|
|
|
def test_run_reinforcing_runs_a_wall_only_check_on_a_wall_mark():
|
|
_findings, log, _excluded = TC.run_reinforcing(
|
|
{'mark': 'WPA655'}, {}, {}, None)
|
|
named = dict((name, state) for (name, state, _note) in log)
|
|
assert named['check_lifting_points'] != TC.NOT_FOR_CLASS_DISPOSITION
|
|
|
|
|
|
def test_flexural_checks_not_built_is_name_evidence_rule_triples():
|
|
assert len(TC.FLEXURAL_CHECKS_NOT_BUILT) >= 5
|
|
for row in TC.FLEXURAL_CHECKS_NOT_BUILT:
|
|
assert len(row) == 3
|
|
for cell in row:
|
|
assert isinstance(cell, str) and cell.strip()
|
|
|
|
|
|
# --- what a flexural piece is ALREADY asked (issue #25, 2026-09-07) -------
|
|
#
|
|
# The first draft of FLEXURAL_CHECKS_NOT_BUILT listed "pick points near
|
|
# 0.2L" and "eccentric strand pattern vs design" as unbuilt. Both are asked
|
|
# today by checks REINFORCING_APPLIES_TO never narrowed: 6.10 compares the
|
|
# lifter stations to PCI with no point-count ceiling, and 4.4/4.6 compare
|
|
# strand across-positions and row elevations to the design. These tests
|
|
# pin that, so the table cannot drift back into calling a running check a
|
|
# gap, and so nobody narrows either check to walls without this failing.
|
|
|
|
def test_flexural_already_asked_names_real_checks_that_apply_to_flexural():
|
|
reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS)
|
|
assert TC.FLEXURAL_CHECKS_ALREADY_ASKED
|
|
for concern, check, row_id in TC.FLEXURAL_CHECKS_ALREADY_ASKED:
|
|
assert concern.strip() and row_id.strip()
|
|
assert check in reinforcing_names, check
|
|
assert TC._reinforcing_applies_to_class(check, 'flexural'), (
|
|
'{0} is listed as already asked on a flexural piece but '
|
|
'REINFORCING_APPLIES_TO narrows it away'.format(check))
|
|
assert check in TC._SKIP_PROBES
|
|
|
|
|
|
def test_pick_position_and_strand_pattern_are_asked_not_gaps():
|
|
asked = dict((check, row_id) for (_c, check, row_id)
|
|
in TC.FLEXURAL_CHECKS_ALREADY_ASKED)
|
|
assert asked['check_lift_positions'] == '6.10'
|
|
assert asked['check_strand_layout'] == '4.4 / 4.6'
|
|
names = [row[0] for row in TC.FLEXURAL_CHECKS_NOT_BUILT]
|
|
# Tombstones: a "6.12" would be 6.10 twice, and the strand row now
|
|
# names only the half the design extract cannot carry.
|
|
assert not [n for n in names if n.startswith('pick point') or '0.2L' in n]
|
|
assert not [n for n in names if 'eccentric' in n]
|
|
assert [n for n in names if 'harp' in n and 'debond' in n]
|
|
|
|
|
|
def test_lift_positions_has_no_point_count_ceiling():
|
|
# 6.9's 20/25 ft ceiling must never leak into 6.10: a flexural piece
|
|
# loses 6.9 by class and keeps 6.10 precisely because 6.10 judges WHERE,
|
|
# never HOW MANY. Inspect the code object rather than run the Revit-
|
|
# shaped gather.
|
|
names = TC.check_lift_positions.__code__.co_names
|
|
consts = [c for c in TC.check_lift_positions.__code__.co_consts
|
|
if isinstance(c, str)]
|
|
assert 'two_point_max_length_ft' not in ' '.join(consts)
|
|
assert 'two_point_absolute_max_ft' not in ' '.join(consts)
|
|
assert 'pci_position_tolerance_in' in ' '.join(consts)
|
|
assert '_pci_recommended_stations' in names
|