Files
willtools-demo/WillTools.extension/tests/test_ticketqc_weight_fallback.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

136 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
"""The `panel.weight` role's generic name-pattern floor in `ticketqc`.
THE DEFECT THIS GUARDS AGAINST
-------------------------------
`read_ticket` used to read the piece's stated weight ONLY through the
plant-mapped role `panel.weight`. On a job whose plant has not mapped that
role the value is `None`, and Panel Lift's weight gate refused every piece
with "The panel has no weight parameter to check against" -- even though the
panel carries its cast weight under a generic name (measured live: a panel's
`MEMBER WEIGHT`-style parameter held it). Reading a model must be generic:
the role is an accelerator, a name-pattern read is the floor (owner,
2026-09-03: pre-fill must work on the next job with no code change).
WHAT IS PINNED HERE
--------------------
a. an instance `MEMBER_WEIGHT` whose display reads "9,974 lb" parses to
9974.0 -- the display carries the unit, so the leading numeric run is
taken before the shared number reader finishes;
b. a `WEIGHT PER FOOT` parameter is NOT a total weight -- 'per' excludes
it, and with nothing else matching the answer is None, never a guess;
c. an instance value beats a type value (the instance is what was set for
this particular piece);
d. an empty instance value falls through to the type's `PANEL WEIGHT`.
THE REVIT STUB
---------------
`vibe.ticketqc` imports from `Autodesk.Revit.DB` at module scope, so it
cannot be imported on host CPython without help. The stub below is the same
one `test_ticketqc_evidence.py` uses and it is PUT BACK afterwards for the
same reason: `sys.modules` is process-wide, and `test_reinforceoffline.py`
proves the offline path pulls in no Revit by asserting no `Autodesk*`
module is loaded.
Nothing under test here calls the Revit API -- the fakes below are plain
objects. If a future edit makes the helper reach for Revit for real, these
tests raise rather than quietly pass.
"""
# pylint: disable=import-error,invalid-name,broad-except
import sys
import types
def _stub_revit():
"""`Autodesk.Revit.DB`, inert, for a host interpreter."""
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 ticketqc as QC # noqa: E402
finally:
for _name in _ADDED:
sys.modules.pop(_name, None)
# --------------------------------------------------------------- fake model
#
# Plain stand-ins for the three surfaces `_weight_by_name_pattern` reads:
# `Parameter.Definition.Name`, `AsValueString()` / `AsString()`, the
# instance's `Parameters` list, and the type element that
# `Document.GetElement(GetTypeId())` hands back.
class _Param(object):
def __init__(self, name, display=None):
self.Definition = type('_Def', (object,), {'Name': name})()
self._display = display
def AsValueString(self):
return self._display
def AsString(self):
return None
class _Element(object):
"""A fake family instance, optionally with a reachable type."""
def __init__(self, instance_params, type_params=None):
self.Parameters = instance_params
self._type = None
if type_params is not None:
self._type = type('_Type', (object,),
{'Parameters': type_params})()
self.Document = type('_Doc', (object,), {})()
self.Document.GetElement = lambda type_id: self._type
def GetTypeId(self):
return 'TYPE-1'
def test_member_weight_instance_display_with_unit_parses():
el = _Element([_Param('MEMBER_WEIGHT', '9,974 lb')])
assert QC._weight_by_name_pattern(el) == 9974.0
def test_weight_per_foot_is_not_a_total_weight_and_nothing_else_matches():
el = _Element([_Param('WEIGHT PER FOOT', '150')])
assert QC._weight_by_name_pattern(el) is None
def test_weight_fragment_matching_an_exclude_is_skipped():
"""The excludes arm, pinned on its own: 'per' must veto a parameter
whose name still carries a positive 'total weight' fragment. The bare
'WEIGHT PER FOOT' above would pass even if the excludes were deleted
(it holds no positive fragment at all); this one would not."""
el = _Element([_Param('TOTAL WEIGHT PER FOOT', '150')])
assert QC._weight_by_name_pattern(el) is None
def test_instance_value_beats_a_type_value():
el = _Element([_Param('MEMBER_WEIGHT', '9,974 lb')],
type_params=[_Param('PANEL_WEIGHT', '8,000 lb')])
assert QC._weight_by_name_pattern(el) == 9974.0
def test_blank_instance_value_falls_through_to_the_types_panel_weight():
el = _Element([_Param('MEMBER_WEIGHT', '')],
type_params=[_Param('PANEL_WEIGHT', '12,500 lb')])
assert QC._weight_by_name_pattern(el) == 12500.0