Files
willtools-demo/WillTools.extension/tests/test_reinforcing_qc.py
T
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

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