WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user