Files
willtools-demo/WillTools.extension/tests/test_extractrules.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

193 lines
6.8 KiB
Python

# -*- coding: utf-8 -*-
"""Plant extraction rule packs.
The property every test here circles is the one the module rests on: a pack
may re-describe a note or move it to a field a person reads, and NOTHING it
can say makes a note disappear. Everything else -- which regex fires, what
sentence comes back -- is detail beside that.
The seven notes used throughout are the real ones, verbatim, from `WPA1.0`
(sample_job, 8" horizontal wall) read on 2026-09-01: the run that
prompted this module, where all seven landed in `unresolved` under one
blanket reason.
"""
import pytest
from vibe import extractrules
from vibe.extractrules import mc
# The seven, in sheet order. Whitespace-normalised the way
# `ticketqc.unresolved_in_view` normalises before anything sees them.
WPA1_NOTES = [
'*USE MC STANDARD REINFORCEMENT FOR CUT DOWN PANELS AND AROUND ALL OPENINGS',
'STRIPPING & BLOCKING DETAIL',
'USE FOR BACK OR EDGE PICK (UNO)',
'STRAND PATTERN',
'STRAND',
"#5 BAR X CONT ONLY NEEDED WHEN LAST PAIR OF STRANDS ARE MORE THAN 1'-0 "
'FROM EDGE OF WALL.',
"STRIPPING & BLOCKING DETAIL < 25'",
]
def entries(texts, reason='no extraction rule matched this note'):
return [{'text': t, 'reason': reason} for t in texts]
def know_with(packs):
"""A knowledge stand-in and a getter over it, so these tests never need
the real layered loader (which wants a Revit doc)."""
store = {'extract.rule_packs': packs}
return store, lambda know, key, default=None: know.get(key, default)
# --------------------------------------------------------------- the gate
def test_no_declaration_loads_no_pack_and_changes_nothing():
"""The honest default: told nothing about an office, recognise nothing."""
know, get = know_with([])
packs, problems = extractrules.load(know, get)
assert packs == []
assert problems == []
given = entries(WPA1_NOTES)
notes, unresolved = extractrules.apply_to(given, packs)
assert notes == []
assert unresolved == given
def test_declared_pack_loads():
know, get = know_with(['mc'])
packs, problems = extractrules.load(know, get)
assert problems == []
assert [p.NAME for p in packs] == ['mc']
def test_a_bare_string_is_accepted_as_one_name():
know, get = know_with('mc')
packs, problems = extractrules.load(know, get)
assert problems == []
assert [p.NAME for p in packs] == ['mc']
def test_unknown_pack_is_refused_loudly_and_nothing_loads():
"""All or nothing. A half-loaded rule set classifies some notes and not
others, and the reader cannot tell which sentences to trust."""
know, get = know_with(['mc', 'no-such-plant'])
packs, problems = extractrules.load(know, get)
assert packs == []
assert len(problems) == 1
assert 'no-such-plant' in problems[0]
assert 'mc' in problems[0] # says what IS available
def test_empty_and_duplicate_names_are_refused():
know, get = know_with(['mc', 'mc'])
_, problems = extractrules.load(know, get)
assert problems and 'twice' in problems[0]
know, get = know_with([''])
_, problems = extractrules.load(know, get)
assert problems and 'empty' in problems[0]
def test_knowledge_default_is_empty():
from vibe import knowledge
assert knowledge.DEFAULTS['extract']['rule_packs'] == []
# ------------------------------------------------- the load-bearing property
def test_no_note_is_ever_dropped():
"""The whole module in one assertion."""
know, get = know_with(['mc'])
packs, _ = extractrules.load(know, get)
given = entries(WPA1_NOTES)
notes, unresolved = extractrules.apply_to(given, packs)
assert len(notes) + len(unresolved) == len(given)
assert (sorted(n['text'] for n in notes + unresolved)
== sorted(WPA1_NOTES))
def test_every_placement_names_the_rule_that_made_it():
"""An unattributable classification is one nobody can argue with."""
know, get = know_with(['mc'])
packs, _ = extractrules.load(know, get)
notes, unresolved = extractrules.apply_to(entries(WPA1_NOTES), packs)
for entry in notes:
assert entry['rule'].startswith('mc/')
for entry in unresolved:
if 'rule' in entry:
assert entry['rule'].startswith('mc/')
def test_a_placement_must_declare_a_known_kind_and_a_rule():
with pytest.raises(ValueError):
extractrules.Placement('vanish', 'mc/x', 'because')
with pytest.raises(ValueError):
extractrules.Placement(extractrules.NOTE, '', 'because')
# ------------------------------------------------------- the mc pack itself
def test_the_seven_notes_land_where_they_should():
know, get = know_with(['mc'])
packs, _ = extractrules.load(know, get)
notes, unresolved = extractrules.apply_to(entries(WPA1_NOTES), packs)
assert sorted(n['text'] for n in notes) == sorted([
'*USE MC STANDARD REINFORCEMENT FOR CUT DOWN PANELS AND AROUND ALL '
'OPENINGS',
'USE FOR BACK OR EDGE PICK (UNO)',
"#5 BAR X CONT ONLY NEEDED WHEN LAST PAIR OF STRANDS ARE MORE THAN "
"1'-0 FROM EDGE OF WALL.",
"STRIPPING & BLOCKING DETAIL < 25'",
])
assert sorted(u['text'] for u in unresolved) == sorted([
'STRIPPING & BLOCKING DETAIL',
'STRAND PATTERN',
'STRAND',
])
def test_a_conditional_detail_title_is_a_note_and_the_bare_one_is_not():
"""The asymmetry this pack exists to get right: the condition is the part
that could change what gets built, so it is never called furniture."""
bare = mc.classify('STRIPPING & BLOCKING DETAIL')
cond = mc.classify("STRIPPING & BLOCKING DETAIL < 25'")
assert bare.kind == extractrules.FURNITURE
assert cond.kind == extractrules.NOTE
assert bare.rule != cond.rule
def test_furniture_keeps_its_place_in_unresolved():
"""`furniture` is not an ignore list -- the line is still written out,
only the sentence beside it changes."""
know, get = know_with(['mc'])
packs, _ = extractrules.load(know, get)
given = entries(['STRAND PATTERN'])
notes, unresolved = extractrules.apply_to(given, packs)
assert notes == []
assert len(unresolved) == 1
assert unresolved[0]['text'] == 'STRAND PATTERN'
assert unresolved[0]['reason'] != given[0]['reason'] # better sentence
assert 'legend' in unresolved[0]['reason']
def test_the_pack_recognises_nothing_it_was_not_shown():
"""A rule with no observation behind it does not belong in the pack, and
a sentence from another producer's sheet must fall straight through."""
for text in ('SEE ARCHITECTURAL DRAWINGS',
'ALL DIMENSIONS ARE NOMINAL',
'USE 6X6 -D3.0/D3.0 W.W.F.',
'',
'STRANDS'):
assert mc.classify(text) is None
def test_classify_is_case_and_padding_tolerant():
assert mc.classify(' strand pattern ') is not None
assert mc.classify('Strand Pattern').kind == extractrules.FURNITURE