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

519 lines
22 KiB
Python

# -*- coding: utf-8 -*-
"""Rendering a takeoff. The tests are about what the page REFUSES to hide.
A takeoff is trusted on sight, so the things worth testing are that an
undercount says the word, that no quantity ever appears without both of its
figures, and -- since the page now folds -- that folding never turns a
caveat into an appendix. Layout is not under test; disclosure is.
THE FOLDING CONTRACT, WHICH IS THE POINT OF HALF OF THIS FILE
---------------------------------------------------------------
The owner's rule (`vibe.foldreport`) is that the answer is the headline and
everything else starts closed, so that an OPEN section means "this needs
you". A takeoff can break that contract in two directions and both are worse
than the flat report was:
* folding something that bites -- the refusal, an undercount, an
unclassified family -- which hides it behind a click nobody makes;
* opening something routine, which spends the contract until an open
section stops meaning anything.
So the tests assert both: a clean takeoff has NOTHING open below the
headline, and each thing that bites opens itself and says why on its header.
"""
# pylint: disable=import-error,invalid-name
from vibe import foldreport as fold
from vibe import takeoffcore as core
from vibe import takeoffreport as report
CFG = core.settings({})
def cache(pieces):
return {'schema': 3, 'built_at': 'once', 'pieces': pieces}
def one(families, **extra):
record = {'name': 'A', 'families': families, 'features': [], 'error': None,
'design_number': None}
record.update(extra)
return cache({'A': record})
def test_the_quantities_are_the_headline_and_carry_both_figures():
"""The order is the answer, so it is what the page opens with."""
out = core.build(one({'P70': 1}), CFG)
head = report.sections(out)[0]
assert head.headline is True
text = '\n'.join(b[1] for b in head.blocks if b[0] == fold.MD)
assert 'Hardware, by part number' in text
columns = [b[1] for b in head.blocks if b[0] == fold.TABLE][0]
assert 'per_mark' in columns and 'per_job' in columns
def test_an_unsourced_multiplier_says_the_word_undercount():
out = core.build(one({'P70': 1}), CFG)
text = report.markdown(out, counts={
'marks': 1, 'pieces': 1, 'multiplier': 1.0,
'by_basis': {'assumed_one': 1}, 'assumed_one': 1})
assert 'UNDERCOUNT' in text
def test_no_multiplier_source_at_all_is_stated_rather_than_implied():
out = core.build(one({'P70': 1}), CFG)
assert 'undercount' in report.markdown(out, counts=None)
def test_an_unclassified_family_is_named_on_the_page():
out = core.build(one({'WIDGET_THING': 5}), CFG)
text = report.markdown(out)
assert 'WIDGET_THING' in text
assert 'refuses to count' in text
def test_the_unclassified_list_is_not_repeated_as_a_declared_exclusion():
"""Two sections saying the same thing reads as two findings."""
out = core.build(one({'WIDGET_THING': 5}), CFG)
text = report.markdown(out)
assert text.count('WIDGET_THING') == 1
def test_an_attached_bar_appears_with_its_parent_named():
out = core.build(one({'SR9001': 6}), CFG, attached={'SR9001': ['P70']})
text = report.markdown(out)
assert 'arrive attached' in text
assert 'P70' in text.split('arrive attached')[1]
def test_every_csv_row_carries_both_figures():
out = core.build(one({'P70': 4}), CFG,
multipliers={'A': (3, 'ticket_files')})
lines = report.csv_totals(out).splitlines()
assert lines[0].startswith('class,key,per_mark_total,per_job_total')
assert lines[1].startswith('hardware,P70,4,12')
def test_the_per_piece_csv_marks_a_piece_nobody_could_judge():
record = {'name': 'A', 'families': {}, 'features': None,
'error': 'members unreadable', 'design_number': None}
out = core.build(cache({'A': record}), CFG)
assert 'COULD_NOT_JUDGE' in report.csv_by_piece(out)
def test_a_comma_in_a_family_name_does_not_shift_a_csv_column():
out = core.build(one({'ODD, NAME': 1}), CFG)
assert '"ODD, NAME"' in report.csv_by_piece(out)
def test_a_nesting_suspicion_says_nothing_was_subtracted():
pieces = {}
for i, n in enumerate([2, 4, 8]):
pieces['P%d' % i] = {'name': 'P%d' % i, 'features': [], 'error': None,
'design_number': None,
'families': {'PARENT': n, 'CHILD': n}}
out = core.build(cache(pieces), CFG)
text = report.markdown(out)
assert 'Nothing has been subtracted' in text
def test_an_unmapped_multiplier_role_is_printed_as_a_refusal():
"""Not as a job in which every mark happens to be one piece."""
out = core.build(one({'P70': 1}), CFG)
text = report.markdown(out, counts={
'marks': 1, 'pieces': 1, 'multiplier': 1.0,
'by_basis': {'assumed_one': 1}, 'assumed_one': 1,
'refusal': 'the role is not mapped'})
assert 'could not be sourced' in text
assert text.index('could not be sourced') < text.index('Quantities to order')
def test_a_source_that_matches_nothing_is_shouted_about():
out = core.build(one({'P70': 1}), CFG)
text = report.markdown(out, counts={
'marks': 1, 'pieces': 1, 'multiplier': 1.0,
'by_basis': {'assumed_one': 1}, 'assumed_one': 1,
'join': {'source_marks': 400, 'matched': 0,
'unmatched_source': ['OTHER_1'], 'unsourced': ['A']}})
assert 'do not match any mark in this takeoff' in text
def test_the_categories_the_multiplier_searched_are_named_on_the_page():
out = core.build(one({'P70': 1}), CFG)
text = report.markdown(out, counts=None, extras={
'multiplier': {'categories': ['OST_StructuralFraming']}})
assert 'OST_StructuralFraming' in text
def test_an_unreadable_control_field_is_named_on_the_page():
out = core.build(one({'P70': 1}), CFG)
text = report.markdown(out, counts=None, extras={
'multiplier': {'unreadable': [
{'id': 9, 'mark': 'A', 'token': 'PL 1/4', 'why': 'not readable'}]}})
assert 'could not turn into pieces' in text
assert 'PL 1/4' in text
# ------------------------------------------------------- the declared scope
#
# A takeoff is an ORDER. Counting somebody else's pieces into it is not an
# extra service, it is a wrong order -- and a takeoff that quietly counted
# only some of the job would be worse still. So the narrowing is stated on
# the page, above the quantities, in all three states.
from vibe import scope as scopelib
from vibe import takeoffgather as gather
def _pop(names):
return {'assemblies': len(set(names)), 'assembly_instances': len(names),
'names': list(names)}
def test_apply_scope_narrows_index_and_population_together():
"""Narrowing one and not the other makes the report claim a hole in the
job where there is only somebody else's work."""
the_cache = cache({'PNL001': {}, 'XX001': {}, 'YY001': {}})
new_cache, new_pop, info = gather._apply_scope(
scopelib.Scope(prefixes=('PNL',)), the_cache,
_pop(['PNL001', 'XX001', 'YY001']))
assert sorted(new_cache['pieces']) == ['PNL001']
assert new_pop['names'] == ['PNL001']
assert info['left_alone'] == [('XX', 1), ('YY', 1)]
assert info['counted_marks'] == 1
def test_apply_scope_never_edits_the_shared_index_in_place():
the_cache = cache({'PNL001': {}, 'XX001': {}})
gather._apply_scope(scopelib.Scope(prefixes=('PNL',)), the_cache,
_pop(['PNL001', 'XX001']))
assert sorted(the_cache['pieces']) == ['PNL001', 'XX001']
def test_apply_scope_undeclared_counts_everything_and_still_says_so():
the_cache = cache({'PNL001': {}, 'XX001': {}})
new_cache, _pop_out, info = gather._apply_scope(
scopelib.Scope(), the_cache, _pop(['PNL001', 'XX001']))
assert sorted(new_cache['pieces']) == ['PNL001', 'XX001']
assert info['declared'] is False
assert 'No scope is declared' in info['line']
def test_the_page_prints_what_it_did_not_count():
takeoff = core.build(one({'SR100': 1}), CFG)
_c, _p, info = gather._apply_scope(
scopelib.Scope(prefixes=('PNL',), note='cladding only'),
cache({'PNL001': {}, 'XX001': {}, 'XX002': {}}),
_pop(['PNL001', 'XX001', 'XX002']))
text = report.markdown(takeoff, extras={'scope': info})
assert 'What this takeoff did not count' in text
assert 'XX 2' in text
assert 'cladding only' in text
# And it stays separate from what the index could not see.
assert 'What this takeoff could not see' in text
def test_the_page_says_so_when_nothing_was_left_out():
takeoff = core.build(one({'SR100': 1}), CFG)
_c, _p, info = gather._apply_scope(
scopelib.Scope(prefixes=('PNL',)), cache({'PNL001': {}}),
_pop(['PNL001']))
text = report.markdown(takeoff, extras={'scope': info})
assert 'nothing was set aside' in text
# ------------------------------------------------------- the folding contract
#
# "If nothing below the headline is open, nothing needs me." Everything from
# here down is that sentence, tested from both sides.
def _clean():
"""A takeoff with nothing wrong with it: every family classified, every
mark in the index, every multiplier sourced."""
the_cache = cache({'A': {'name': 'A', 'families': {'P70': 2},
'features': [], 'error': None,
'design_number': '101'}})
return core.build(the_cache, CFG,
multipliers={'A': (3, 'control_numbers')},
population={'assemblies': 1, 'assembly_instances': 1,
'names': ['A']})
CLEAN_COUNTS = {'marks': 1, 'pieces': 3, 'multiplier': 3.0,
'by_basis': {'control_numbers': 1}, 'assumed_one': 0,
'sources': {'control_numbers': 1, 'ticket_files': 0,
'assembly_instances': 0},
'join': {'source_marks': 1, 'matched': 1,
'unmatched_source': [], 'unsourced': []}}
def _tail(takeoff, counts=None, extras=None):
return report.sections(takeoff, counts, extras=extras)[1:]
def _open(takeoff, counts=None, extras=None):
return [s.key for s in fold.sections_with_problems(
_tail(takeoff, counts, extras))]
def test_a_takeoff_with_nothing_wrong_opens_nothing_below_the_headline():
"""The contract itself. If a routine press opens a section, an open
section stops meaning 'this needs you' and the whole page is furniture."""
assert _open(_clean(), CLEAN_COUNTS) == []
def test_every_folded_section_says_what_it_holds():
"""A header that says only 'details' tells the reader nothing about
whether to open it, and folded is only acceptable while it is not gone."""
for section in _tail(_clean(), CLEAN_COUNTS):
assert section.summary, section.key
def test_the_headline_says_what_multiplied_the_per_job_column():
"""The totals can be lifted off this page alone, so the thing that scaled
them has to be on it -- not only in the section that owns it."""
head = report.sections(_clean(), CLEAN_COUNTS)[0]
text = '\n'.join(b[1] for b in head.blocks if b[0] == fold.MD)
assert 'control_numbers 1' in text
assert 'per_job' in text and 'per_mark' in text
def test_a_refused_multiplier_is_open_and_printed_whole():
"""The refusal is the one sentence on the page saying the numbers were
never sourced. Folding it into an appendix would be the exact failure the
flat report existed to prevent."""
out = core.build(one({'P70': 1}), CFG)
counts = dict(CLEAN_COUNTS)
counts['refusal'] = ('the role `member.control_number` is not mapped, so '
'the model was not asked how many pieces each mark '
'is')
sections = report.sections(out, counts)
multiplier = [s for s in sections if s.key == 'multiplier'][0]
assert multiplier.is_open is True
assert 'not asked how many pieces' in '\n'.join(
b[1] for b in multiplier.blocks if b[0] == fold.MD)
# and the headline itself does not let the totals be read without it
head = '\n'.join(b[1] for b in sections[0].blocks if b[0] == fold.MD)
assert 'UNDERCOUNT' in head
def test_a_refused_multiplier_survives_a_dead_toggle():
"""pyRevit's panel is an IE-engine WebBrowser and the fold is an inline
onclick. If the script does not run, an open section must still be on the
page as plain HTML."""
out = core.build(one({'P70': 1}), CFG)
counts = dict(CLEAN_COUNTS)
counts['refusal'] = 'nobody mapped the control-number role'
html = fold.html(_tail(out, counts))
assert 'nobody mapped the control-number role' in html
assert 'display:block' in html
def test_the_panel_html_is_one_line_and_uses_nothing_this_engine_lacks():
html = fold.html(_tail(_clean(), CLEAN_COUNTS))
assert chr(10) not in html
assert '<details' not in html and 'classList' not in html
def test_an_unclassified_family_opens_its_own_section():
out = core.build(one({'WIDGET_THING': 5}), CFG)
assert 'unclassified' in _open(out, CLEAN_COUNTS)
def test_marks_counted_once_open_and_are_named():
out = core.build(one({'P70': 1}), CFG)
counts = dict(CLEAN_COUNTS)
counts['assumed_one'] = 1
section = [s for s in _tail(out, counts)
if s.key == 'multiplier-assumed'][0]
assert section.is_open is True
text = '\n'.join(b[1] for b in section.blocks if b[0] == fold.MD)
assert 'UNDERCOUNT' in text
assert 'A' in text # the mark itself, not a tally
def test_a_source_whose_marks_match_nothing_opens():
out = core.build(one({'P70': 1}), CFG)
counts = dict(CLEAN_COUNTS)
counts['join'] = {'source_marks': 400, 'matched': 0,
'unmatched_source': ['OTHER_1'], 'unsourced': ['A']}
assert 'multiplier-join' in _open(out, counts)
def test_an_unreadable_control_field_opens_rather_than_folding():
out = core.build(one({'P70': 1}), CFG)
assert 'multiplier-unreadable' in _open(
out, CLEAN_COUNTS,
{'multiplier': {'unreadable': [{'id': 9, 'mark': 'A',
'token': 'PL 1/4',
'why': 'not readable'}]}})
def test_where_the_multiplier_swept_is_routine_and_stays_folded():
"""Naming the categories is provenance, not a problem. It opens nothing."""
assert _open(_clean(), CLEAN_COUNTS,
{'multiplier': {'categories': ['OST_StructuralFraming'],
'elements': 12}}) == []
def test_a_nesting_suspicion_opens_itself():
pieces = {}
for i, n in enumerate([2, 4, 8]):
pieces['P%d' % i] = {'name': 'P%d' % i, 'features': [], 'error': None,
'design_number': None,
'families': {'PARENT': n, 'CHILD': n}}
out = core.build(cache(pieces), CFG)
assert 'nesting' in _open(out, CLEAN_COUNTS)
def test_a_mark_in_the_model_the_index_never_saw_opens_coverage():
"""Those pieces are missing from the order. A hole in the job is not a
routine coverage line, whatever else coverage usually is."""
out = core.build(one({'P70': 1}), CFG,
population={'assemblies': 9, 'assembly_instances': 9,
'names': ['A']})
assert 'coverage' in _open(out, CLEAN_COUNTS)
def test_a_piece_the_index_could_not_judge_opens_coverage():
record = {'name': 'A', 'families': {}, 'features': None,
'error': 'members unreadable', 'design_number': None}
out = core.build(cache({'A': record}), CFG,
population={'assemblies': 1, 'assembly_instances': 1,
'names': ['A']})
assert 'coverage' in _open(out, CLEAN_COUNTS)
# ------------------------------------------- what one exported ticket covers
#
# `nomenclature.ticket_unit` decides whether the exported tickets counted at
# all. Declared, it is one routine sentence and opens nothing. Undeclared, it
# is a refusal -- and it opens the section at the point where the missing
# declaration stopped being a formality and started costing marks.
def _with_unit(unit, **plan):
counts = dict(CLEAN_COUNTS)
base = {'key': 'nomenclature.ticket_unit', 'unit': unit,
'declared': unit is not None, 'recognised': True,
'order': ('control_numbers', 'ticket_files',
'assembly_instances'),
'withdrawn': (), 'withdrawn_marks': 0, 'cost': 0,
'reason': 'one ticket per control number, so a ticket is a piece',
'refusal': None}
base.update(plan)
counts['ticket_unit'] = base
return counts
def test_a_declared_ticket_unit_is_stated_and_opens_nothing():
"""Somebody answered the question. That is not a problem, and spending an
open section on it would spend the folding contract."""
counts = _with_unit('control_number')
assert _open(_clean(), counts) == []
text = report.markdown(_clean(), counts)
assert 'one ticket per control number' in text
def test_the_ticket_unit_is_on_the_folded_header_either_way():
"""A reader who never opens the multiplier section still learns whether
the run knew what a ticket was."""
section = [s for s in _tail(_clean(), _with_unit('control_number'))
if s.key == 'multiplier'][0]
assert 'ticket unit: control_number' in section.summary
undeclared = [s for s in _tail(_clean(), _with_unit(
None, declared=False, refusal='nobody declared it',
reason='nobody declared it', withdrawn=('ticket_files',)))
if s.key == 'multiplier'][0]
assert 'NOT DECLARED' in undeclared.summary
def test_an_undeclared_ticket_unit_that_cost_marks_opens_the_section():
counts = _with_unit(None, declared=False, cost=7, withdrawn_marks=9,
withdrawn=('ticket_files',),
order=('control_numbers', 'assembly_instances'),
refusal='`nomenclature.ticket_unit` is not declared',
reason='`nomenclature.ticket_unit` is not declared')
assert 'multiplier' in _open(_clean(), counts)
section = [s for s in _tail(_clean(), counts)
if s.key == 'multiplier'][0]
assert '7 mark(s) lost their only multiplier source' in section.problem
text = '\n'.join(b[1] for b in section.blocks if b[0] == fold.MD)
assert 'nomenclature.ticket_unit' in text
def test_an_undeclared_ticket_unit_that_cost_nothing_is_said_but_not_opened():
"""No exported tickets to withdraw means the missing declaration moved no
number. It is still printed -- folded is not gone."""
counts = _with_unit(None, declared=False, cost=0,
withdrawn=('ticket_files',),
order=('control_numbers', 'assembly_instances'),
refusal='not declared, so the tickets were not used',
reason='not declared, so the tickets were not used')
assert _open(_clean(), counts) == []
assert 'not declared, so the tickets were not used' in report.markdown(
_clean(), counts)
def test_a_withdrawn_source_is_shown_as_withdrawn_not_as_one_that_found_none():
"""It knew 57 marks and contributed none of them. A table showing only
what it knew would read as if it had."""
counts = _with_unit('piece_mark', withdrawn=('ticket_files',),
order=('control_numbers', 'assembly_instances'),
reason='one ticket covers a whole mark')
counts['sources'] = {'control_numbers': 1, 'ticket_files': 57,
'assembly_instances': 0}
section = [s for s in _tail(_clean(), counts)
if s.key == 'multiplier'][0]
rows = [b[2] for b in section.blocks if b[0] == fold.TABLE][-1]
assert ['ticket_files', '57', 'NO'] in rows
assert ['control_numbers', '1', 'yes'] in rows
# and a declaration nobody has to act on does not open the section
assert _open(_clean(), counts) == []
def test_the_page_carries_no_ticket_unit_claim_when_it_was_given_none():
"""The report never invents provenance it was not handed -- the same rule
`extras` already follows."""
text = report.markdown(_clean(), CLEAN_COUNTS)
assert 'ticket unit' not in text
def test_the_declared_scope_is_folded_because_somebody_declared_it():
"""Work left alone on purpose is not a problem. It is still printed."""
takeoff = _clean()
_c, _p, info = gather._apply_scope(
scopelib.Scope(prefixes=('A',), note='cladding only'),
cache({'A': {}, 'XX001': {}}), _pop(['A', 'XX001']))
keys = dict((s.key, s) for s in _tail(takeoff, CLEAN_COUNTS,
{'scope': info}))
assert keys['scope'].is_open is False
assert 'left alone' in keys['scope'].summary
def test_the_long_tables_stay_folded_however_long_they_get():
"""The by-piece breakdown is the biggest thing on the page and the least
read. Length is not a problem; it is just length."""
pieces = dict(('P%03d' % i,
{'name': 'P%03d' % i, 'families': {'P70': i + 1},
'features': [], 'error': None, 'design_number': None})
for i in range(150))
out = core.build(cache(pieces), CFG,
population={'assemblies': 150, 'assembly_instances': 150,
'names': sorted(pieces)})
counts = dict(CLEAN_COUNTS)
counts['by_basis'] = {'control_numbers': 150}
keys = dict((s.key, s) for s in _tail(out, counts))
assert keys['by-piece'].is_open is False
assert keys['judgements'].is_open is False
assert keys['attached'].is_open is False
assert '150 piece(s)' in keys['by-piece'].summary
def test_the_written_report_still_carries_every_folded_sentence():
"""Folded is not gone: the `.md` beside the model is the unfolded copy,
so a fold that will not open costs a file open and never a sentence."""
text = report.markdown(_clean(), CLEAN_COUNTS)
assert 'rule:*' in text # the judgements section
assert 'One row per piece' in text # the by-piece section
assert 'None of the counted bar marks' in text # the attached section
assert 'marks in the model but NOT indexed' in text